Stephan Schmidt - August 4, 2025
Securing Credentials in Golang With Systemd
From a solo entrepreneur
TL;DR: Use systemd's credential injection feature to manage secrets in Go applications—it provides a middle ground between simple ENV variables and full secret managers by creating virtual credential files that your app reads via `$CREDENTIALS_DIRECTORY`, with built-in support for encryption, TPM2 hardware security, and filesystem isolation. This approach requires no code changes to switch credential sources later and eliminates the need to run separate infrastructure like Vault for smaller teams.
When running a Go binary sooner or later, you ask yourself where to get credentials for database access, external services like sending emails and encryption. First, you can’t put them into source control, or they will end up on GitHub. Any supply chain attack that can access your code during a build pipeline can access your secrets.
You can put the secrets in your binary, but anyone with access to the binary can find the secrets. If you use the same binary in testing and development, there is a chance that the binary contains production secrets, if you haven’t properly used build tags.
The Twelve Factor App has secrets stored in ENV variables This is a secure way, but if your environment doesn’t already support this (injecting from a cloud manager), handling those ENV variables can be a pain.
The enterprise way is to use a secret manager like OpenBao. This solution has a lot of appeals, it supports key rotation, more security features and additional niceties. It can rotate the passwords for your database and give your application the new ones. It is a fantastic piece of code. But for a solo entrepreneur, it is another application to run—with failover this might be too much work.
One solution that strikes a good balance between complexity and security is using systemd to manage those credentials. If you already use systemd for deployments then this is the easiest way. Systemd does a lot of things and also can inject credentials into your running applications.
The way systemd does this is by creating a virtual directory with a file for your application to read, that contains those credentials.
It gives your application an environment variable $CREDENTIALS_DIRECTORY that contains a directory to read from. The virtual file can come from different sources. Configuring it to be read from a file:
[Service]
LoadCredential=foobar:/etc/myfoobarcredential.yaml
This injects the file /etc/myfoobarcredential.yaml as a file $CREDENTIALS_DIRECTORY/foobar into your application to read.
db_user: foo
db_password: bar
The file with the credentials needs the correct permissions, so only systemd can read the file, not any user! (systemd can encrypt the file and credentials).
credentialsDir := os.Getenv("CREDENTIALS_DIRECTORY")
if credentialsDir == "" {
log.Error().Msg("CREDENTIALS_DIRECTORY environment variable is not set")
return nil
}
With Viper for your configuration manager, the file then can be read with
viper.SetConfigName("foobar") // name of config file
viper.SetConfigType("yaml") // specify the config file format
viper.AddConfigPath(credentialsDir) // path to look for the config file
Now the credentials can be accessed in your application just like any other config.
For additional security, consider a library like Memguard to secure credentials in memory (against core dumps, for example).
The benefits of using systemd over reading the file directly:
systemdability to load credentials from somewhere else, not a real file. Credentials can be injected from outside your VM, from the BIOS, or if your environment (e.g., AWS) does support TPM2, from a secured source.- You can change the source of the credentials in the future without changes to your code
systemdcan read an encrypted file and get the key from a secure location - no key management needed in your applicationsystemdcan secure your application to disallow reading any other files, so an exploit in your app is unable to access the file system while you can still read the credentials.
About me: Hey, I'm Stephan, I help CTOs with Coaching, with 40+ years of software development and 25+ years of engineering management experience. I've coached and mentored 80+ CTOs and founders. I've founded 3 startups. 1 nice exit. I help CTOs and engineering leaders grow, scale their teams, gain clarity, lead with confidence and navigate the challenges of fast-growing companies.
