Best way to keep environment variables secret

Godot Version

4.3

Question

I would like to add some automated logging and event tracking in my test builds. For development I am using an addon godot_dotenv so that I can keep a .env file out of my repository but keep api keys etc there. I am wondering about how this will be handled when I export my game. I would rather keep from, even temporarily, hard coding any api key into the source, but I am assuming continuing to use this plugin as is will require me to ship the test builds with a .env file that will have all my api keys in it.

What is considered the best practice for keeping secrets a secret when exporting a game with the godot engine. I am using gdscript. Iā€™d love it if there was a way to load and encrypt a resource at export time.

Update after some questions:
I think I need to clarify a bit. I would like to keep my keys in an easy to modify place that out of source control --so no hard coding. I understand that once the thing ships then all bets are off and the best way to create some sort of client server secret management system but I donā€™t care that much.

This question is more about finding a way to read from a source, be it a resource or an environment variable, and make it behave like a hard coded variable.

Thereā€™s an Encryption tab in the export tool. Just use that and your game files are encrypted.

I would add that it depends on what kind of keys/secrets they are. Depending on how well secured they need to be, the ā€˜best practiceā€™ might be to not ship the keys to the client at all and instead keep them on infrastructure you control.

Agreed @pmoosi

Ideally if this is a client/server architecture, you would be generating a new client-specific private key upon install of the game, use that to connect to the server, and then the server would store all your api keys so that the information is never on the userā€™s box. Then if say you need to make an API call somewhere you can do the call and return the results. This also allows you to encapsulate the API call on the server, and if you say decided to use a different service, you could make that change without needing to push and update to the user.

This post does not answer the updated question. Left for historical purposes.

Original post

From a more abstract viewpoint:

Firstly, whatever you ship to the client you can consider to be publicly visible and open source. It doesnā€™t matter how well you try to hide it and encrypt it - if your app can decrypt it on the client machine, then so can the user - with enough effort, of course.

So, you either make it so that itā€™s not worth the effort, or you donā€™t rely on anything you shipped with the app.

The first approach is viable if the credentials arenā€™t really that important. If itā€™s just some usage logging in aggregate form, then quite likely nobody will care to hack that. You can use some light encryption or maybe hardcode the key in the GDScript source. Either way itā€™s mildly inconvenient to access (you need to decompile stuff and read the decompiled code), and the effort that would take isnā€™t worth it.

The second approach means that the credentials need to come from elsewhere. This would be normally some sort of user account that they have to create on your server. Now you donā€™t need to hide or encrypt anything! The user will supply the username and password, and you just use those to authenticate to your server. If someone starts to misbehave, you can immediately see who it is, and revoke their (and only their) access.

This is the preferred approach when you have more sensitive data to handle where people might be incentivized to get direct access to your API - for example high scores or multiplayer sessions.

I think I need to clarify a bit. I would like to keep my keys in an easy to modify place that out of source control --so no hard coding. I understand that once the thing ships then all bets are off and the best way to create some sort of client server secret management system but I donā€™t care that much.

This question is more about finding a way to read from a source, be it a resource or an environment variable, and make it behave like a hard coded variable.

The easiest way to do that is to simply include that file in your gitignore file so itā€™s never in source control.

1 Like

Another idea added to the brainstorming:

You could add a simple settings menu or some kind of ā€œlogin formā€ where the user can paste the API key. The key can be persisted to the user directory via ConfigFile, ResourceSaver or JSON.

This way, you donā€™t have to care about version control or plugins. It also allows your users (in case you want to distribute your game/app) to use their own API keys. And for you, you only have to enter the key once and it will automatically be re-used on next start.

1 Like

I have rewritten RCON functionality in just GDScript with TCPServer/StreamPeerTCP and set the server password via a ConfigFile like @namelessvoid mentioned. You can fallback to a default ConfigFile from res:// whenever the version in user:// is invalid, hereā€™s a configuration.gd autoload I use in any new project:

extends Node

var defaults: ConfigFile = ConfigFile.new()
var settings: ConfigFile = ConfigFile.new()

func _ready() -> void:
	defaults.load("res://autoloads/configuration.cfg")
	
	if !FileAccess.file_exists("user://configuration.cfg") or settings.load("user://configuration.cfg") != OK:
		revert_settings()
		return
	else:
		load_settings()
		return

func load_settings() -> void:
	var defaults_sections: PackedStringArray = []
	var settings_sections: PackedStringArray = []
	
	var defaults_keys: Array = []
	var settings_keys: Array = []
	
	defaults_sections = defaults.get_sections()
	settings_sections = settings.get_sections()
	
	if settings_sections != defaults_sections:
		revert_settings()
		return
	else:
		pass
	
	for section in defaults_sections:
		defaults_keys.append(defaults.get_section_keys(section))
		continue
	
	for section in settings_sections:
		settings_keys.append(settings.get_section_keys(section))
		continue
	
	if settings_keys != defaults_keys:
		revert_settings()
		return
	else:
		#Sample modification and startup of my tcp.gd autoload based on ConfigFile
		TCP.binding = settings.get_value("Networking", "Binding", "127.0.0.1")
		TCP.port = settings.get_value("Networking", "Port", 42069)
		TCP.headless = settings.get_value("Networking", "Headless", false)
		TCP.rconpwd = settings.get_value("Networking", "Password", "ChangeMe!")
		TCP.boot()
		return

func revert_settings() -> void:
	settings = defaults
	settings.save("user://configuration.cfg")
	load_settings()
	return

If you want them to be less-persistent than a ConfigFile but moreso than hardcoding, then having users modify their executableā€™s shortcut to include a parameter would allow for OS.get_cmdline_args( ) handling, i.e. $MyGame.exe --api-key $MyApiKeyValue

1 Like

I like this approach best so far. I would still like some way to make it get included in the source as a part of the export instead of requiring it to be another file that is outside the executable. Making it an argument would satisfy that. I really donā€™t want to have to use a client account or anything like that for this --at least not while the game is still under development.

Maybe that is a better question. How can I have the export embed a variable in the pck before encryption?

Thatā€™s where specifying your default in the res:// version of the ConfigFile kicks in. That way if the user never supplies one in their user:// version your version from res:// sets that value for them.

And this directly answers your final question, having it in the res:// version would include it in the PCK as long as your export settings are not filtering out *.cfg. Donā€™t confuse the load_settings() default values (third parameter on my TCP. variable assignments) with the default values from the res:// version of the ConfigFile, I just do both for consistencyā€™s sake. But either way, whether I include .cfg, .db, etc. as long as my export settings are including them they stay in the PCK (or EXE if embedding the PCK in the executable which is my go-to export option).

Edit: I stand corrected! PCK-only exports donā€™t include asset res:// content, only full executable exports do. Do you ever plan on just shipping PCKs? What use case is at play here where you wouldnā€™t just do a full executable export?

I have done several projectsā€¦ demand is fast, project leads donā€™t care, and overall ICT management either doesnā€™t know or has no clue.

Iā€™ve worked with some of the leading solutions out there, but in the end, theyā€™re all just separate programs with their own quirks.

For your own project, keep secrets in environment variables or use a dedicated secrets manager like HashiCorp Vault, AWS Secrets Manager, or Dopplerā€”just donā€™t hardcode them in your repo.

At the very least, .env files with proper .gitignore rules are better than nothing.

Security might not be fun, but a leaked API key definitely isnā€™t.

1 Like

I love Vault.

I donā€™t exactly understand the purpose of a PCK only export so I donā€™t know if I would ever use one. Is that for an update?

Yea just for updates, DLCs, those sorts of things. People are mentioning Vault, SM, Doppler, etc. which are valid recommendations but the overall post made it sound like you needed to be able to modify these things more nimbly than even those. When I use AWS in my Godot projects, I just rely on the systemā€™s CLI/SDK installs and local keys instead of giving Godot anything AWS cred-related.