The credentials module provides a secure way to manage both sensitive and non-sensitive configurations for Go applications. It supports encrypted configuration files, environment variables, and plain text configuration files, allowing users to easily manage settings securely and flexibly.
- Encrypted Credentials: Store sensitive configuration values (like API keys) in an encrypted file.
- Environment Variables: Environment variables can override configurations, providing additional flexibility.
- Custom Configuration Structs: Define your own configuration struct and pass it to the module, making it adaptable to any configuration needs.
Credentials are sealed with AES-256-GCM. GCM is authenticated: if the file is modified by anyone without the master key, decryption fails loudly instead of returning altered contents.
Versions before v2 used AES-CFB, which is unauthenticated. Under CFB a
plaintext bit can be flipped by flipping the matching ciphertext bit — no key
required, and nothing detects it. Go's standard library now marks CFB deprecated
for this reason. If your credentials.yml.enc predates this change, see
Migrating.
The master key is written 0600. The encrypted file is written 0644 — it is
meant to be committed; the key beside it never should be, which is what the
bundled .gitignore enforces.
go install github.com/roonglit/credentials/cmd/credentials@latestOne shared file, or one file per environment with its own key:
config/credentials.yml.enc + config/master.key shared
config/credentials/staging.yml.enc + config/credentials/staging.key
config/credentials/production.yml.enc + config/credentials/production.key
Use separate files for anything but development. The point is not tidiness,
it is key separation: with one shared key, anyone who can boot the app locally
can decrypt production. A new hire gets master.key on their first day and it
opens the production database URL. Separate files mean a leaked development key
leaks only development.
credentials edit # shared file
credentials edit -e production # config/credentials/production.yml.enc
credentials show -e stagingA per-environment file is flat — the file already is that environment, so there is no section to nest under:
# config/credentials/production.yml.enc
DATABASE_URL: postgres://…
API_TOKEN: …The shared file keeps the older shape, a section per environment, and is still
what Read falls back to when no per-environment file exists. Projects that
have only the shared file need no changes.
reader := credentials.NewConfigReader() // defaults to ./config
err := reader.Read(os.Getenv("APP_ENV"), &cfg)Set CREDENTIALS_KEY to a hex key to skip the key file entirely — the role
RAILS_MASTER_KEY plays in Rails, and what you want in a container.
credentials edit # decrypt, open in $EDITOR, re-encrypt
credentials show # print the decrypted contents to stdout
credentials migrate # re-encrypt an old file, no editor neededEnvironment:
| variable | meaning |
|---|---|
CREDENTIALS_DIR |
directory holding master.key and credentials.yml.enc (default config) |
VISUAL, EDITOR |
editor used by edit (default vi) |
CREDENTIALS_ALLOW_LEGACY |
set to 0 for strict mode: refuse pre-v2 files instead of warning |
CREDENTIALS_KEY |
hex key, used instead of any key file |
credentials edit creates both files on first run. It refuses to generate a new
master key when an encrypted file already exists, since that would make the
existing file permanently unreadable.
Upgrading does not break anything. Old files still open, the exported API is unchanged from v1.0.0, and no code change is required to bump the dependency. Reading a pre-v2 file prints one warning per file:
credentials: config/credentials.yml.enc is in the legacy unauthenticated format.
It cannot detect tampering. Run `credentials migrate` to fix it permanently.
Migrate once, per project:
credentials migrateIt decrypts with the old format and rewrites with the new one, using the same
master key. Commit the result; the warning stops. credentials edit migrates as
a side effect of saving, so a file you were editing anyway needs nothing extra.
Writes are always in the current format — there is no way to write a legacy file, which is what stops the old format lingering.
Set CREDENTIALS_ALLOW_LEGACY=0 for strict mode, where an unmigrated file is an
error rather than a warning. Worth turning on in CI once you have migrated, so a
file cannot quietly regress.
Run the following command to install the credentials package into your project:
go get github.com/roonglit/credentials/pkg/credentialsDefine a custom struct with fields that match the configuration keys in credentials.yml.enc. Use mapstructure tags to map the struct fields to the configuration keys.
package main
import (
"fmt"
"log"
"time"
"github.com/roonglit/credentials/pkg/credentials"
)
// Define your custom configuration struct
type MyConfig struct {
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
DBUri string `mapstructure:"DB_URI"`
AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`
RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
}Use ConfigReader to load and decrypt configurations. The Read method will populate your custom struct with values from credentials.yml.enc and environment variables.
Here's how to initialize and use the ConfigReader with the default configuration folder:
func main() {
// Initialize the ConfigReader with the default config folder
reader := credentials.NewConfigReader()
// User-defined configuration struct
var config MyConfig
// Read configuration with mode "debug" or "production"
if err := reader.Read("debug", &config); err != nil {
log.Fatalf("Failed to read configuration: %v", err)
}
fmt.Printf("Loaded Configuration: %+v\n", config)
}If your configuration folder is different, you can provide the path as an argument:
reader := credentials.NewConfigReader("path/to/config")This project is licensed under the MIT License.
Contributions are welcome! If you have suggestions or improvements, feel free to open a pull request.