Encrypted credentials management for Go projects - inspired by Rails credentials.
Stop juggling .env files and environment variables. Keep your secrets
encrypted in your repo, share them safely with your team, and access them with
a clean API.
First, create stores for development and production environments:
gosecrets init # secrets/development.key + secrets/development.enc
gosecrets init --env production # secrets/production.key + secrets/production.encEach init prints a master key — save it somewhere safe. You will need
this key to decrypt your credentials in deployment (e.g. Portainer, CI).
Locally, the key file on disk is used automatically.
You should see something like:
master key: e5d0529e.................................................
save this key somewhere safe, you need it to decrypt your credentials.
Edit your development config: gosecrets edit (pops up your default $EDITOR), edit yaml:
stripe:
key: sk_test_xxx
webhook_secret: whsec_test_abcEdit your production config: gosecrets edit --env production:
stripe:
key: sk_live_yyy
webhook_secret: whsec_live_defFix your .gitignore file
secrets/*.key
Keep this in mind! NEVER COMMIT *.key files!
Now, in your Go project, your code is the same everywhere:
secrets, err := gosecrets.Load()
if err != nil {
// handle your error
}
fmt.Println(secrets.String("stripe.key"))
// development: sk_test_xxx
// production: sk_live_yyygosecrets.Load() picks the environment automatically from GOSECRETS_ENV.
Default is development. In production, just set:
export GOSECRETS_ENV=production
export GOSECRETS_PRODUCTION_KEY="your-master-key-here"
# or whatever your env is:
# export GOSECRETS_STAGING_KEY="your-master-key-here"
# export GOSECRETS_FOO_KEY="your-master-key-here"You can choose any environment name, such as foo:
export GOSECRETS_ENV=foo
gosecrets init # creates secrets/foo.key + secrets/foo.enc
gosecrets edit # add your secretssecrets, err := gosecrets.Load() // picks up GOSECRETS_ENV=foo automatically| Environment | GOSECRETS_ENV |
.enc file | Key from | Key ENV VAR |
|---|---|---|---|---|
| development | (empty or unset) | secrets/development.enc |
secrets/development.key (disk) |
GOSECRETS_DEVELOPMENT_KEY |
| production | production |
secrets/production.enc |
env var | GOSECRETS_PRODUCTION_KEY |
| <any-name> | <any-name> |
secrets/<any-name>.enc |
env var | GOSECRETS_<ANY-NAME>_KEY |
By default, gosecrets creates and looks for the secrets/ directory in the
current working directory. If your project layout requires a different
location, use the --root flag or the GOSECRETS_ROOT environment variable.
Important: The
--rootflag only accepts relative paths within the project directory. Absolute paths and..traversal are rejected. For paths outside the project (CI/CD, Docker), use theGOSECRETS_ROOTenvironment variable.
# create secrets under ./deploy instead of current directory
gosecrets init --root ./deploy --env production
# → ./deploy/secrets/production.key
# → ./deploy/secrets/production.enc
# edit, show, get all support --root
gosecrets edit --root ./deploy --env production
gosecrets show --root ./deploy --env production
gosecrets get database.password --root ./deploy --env production
# --root=<path> syntax also works
gosecrets init --root=config
# these are rejected by the CLI:
gosecrets init --root /app # ✘ absolute path
gosecrets init --root ../outside # ✘ escapes project directory
gosecrets init --root= # ✘ empty valueIn CI/CD pipelines and Docker containers, the project root is often an absolute
path like /app. Use GOSECRETS_ROOT for these cases:
# Docker / CI — set absolute path via env var
export GOSECRETS_ROOT=/app
gosecrets init --env production # uses /app/secrets/
gosecrets edit --env production # uses /app/secrets/# Dockerfile example
ENV GOSECRETS_ROOT=/app
ENV GOSECRETS_ENV=production
ENV GOSECRETS_PRODUCTION_KEY=your-master-key-hereThe --root flag always takes precedence over GOSECRETS_ROOT.
When using gosecrets as a Go library, WithRoot() accepts both relative
and absolute paths — no restriction:
// load from /app/secrets/production.enc (absolute — common in Docker)
secrets, err := gosecrets.Load(
gosecrets.WithRoot("/app"),
gosecrets.WithEnv("production"),
)
// load from ./deploy/secrets/development.enc (relative)
secrets, err := gosecrets.Load(
gosecrets.WithRoot("./deploy"),
)Without WithRoot(), the library reads from ./secrets/ relative to the
working directory — same as the CLI.
| Priority | Source | Accepts | Example |
|---|---|---|---|
| 1 (highest) | --root flag |
relative only | --root ./deploy |
| 2 | GOSECRETS_ROOT env var |
relative or absolute | export GOSECRETS_ROOT=/app |
| 3 (default) | Current working directory | — | ./secrets/ |
your-project/
├── deploy/ ← --root ./deploy
│ └── secrets/
│ ├── production.enc ← encrypted YAML (committed)
│ └── production.key ← decryption key (.gitignore!)
├── secrets/ ← default (no --root)
│ ├── development.enc
│ └── development.key
├── main.go
└── ...
gosecrets stores your secrets encrypted inside your repository. Only
the master key stays outside version control.
Each environment gets its own .enc / .key pair inside secrets/:
your-project/
├── secrets/
│ ├── development.enc # encrypted YAML — committed to git
│ ├── development.key # decryption key — add to .gitignore!
│ ├── production.enc # encrypted YAML — committed to git
│ └── production.key # decryption key — add to .gitignore!
In development, the key file on disk is enough. In production/CI, set
GOSECRETS_<ENV>_KEY (e.g. GOSECRETS_PRODUCTION_KEY) as an environment
variable and never deploy the key file.
.keyfiles are for local work, env vars are for deployment. When both exist, the env var takes precedence.
Credentials are encrypted with AES-256-GCM (authenticated encryption). Each write generates a fresh random nonce - the same plaintext produces different ciphertext every time.
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│ You edit │────▶│ gosecrets │────▶│ <env>.enc │
│ YAML │ │ encrypts │ │ (committed) │
└──────────┘ └──────────────┘ └─────────────────┘
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│ Your app │◀────│ gosecrets │◀────│ <env>.enc │
│ reads │ │ decrypts │ │ + master key │
└──────────┘ └──────────────┘ └─────────────────┘
Library:
go get -u github.com/bilustek/gosecretsCLI Tool via Homebrew:
brew tap bilustek/tap
brew install gosecretsor via go install:
go install github.com/bilustek/gosecrets/cmd/gosecrets@latestRun gosecrets:
$ gosecrets
gosecrets - encrypted credentials for Go projects
Usage:
gosecrets init [--env ENV] [--root DIR] Initialize a new credential store
gosecrets edit [--env ENV] [--root DIR] Edit credentials in $EDITOR
gosecrets show [--env ENV] [--root DIR] Print decrypted credentials to stdout
gosecrets get KEY [--env ENV] [--root DIR] Get a specific value (dot notation)
gosecrets version, --version, -v Show version
gosecrets help, --help, -h Show this help
gosecrets completion bash Output bash completion script
Environment:
GOSECRETS_ROOT Root directory for secrets/ (default: current directory)
GOSECRETS_ENV Environment name (default: development)
GOSECRETS_MASTER_KEY Master key (overrides all key files)
GOSECRETS_<ENV>_KEY Environment-specific key (e.g. GOSECRETS_PRODUCTION_KEY)
EDITOR / VISUAL Preferred text editor
Examples:
gosecrets init Creates ./secrets/development.{key,enc}
gosecrets init --env production Creates ./secrets/production.{key,enc}
gosecrets init --root ./deploy --env production Creates ./deploy/secrets/production.{key,enc}
gosecrets edit Opens credentials in your editor
gosecrets get database.password Prints a specific valueEnable tab completion for subcommands, --env values, --root directories, and get keys:
# add to your ~/.bashrc or ~/.bash_profile
eval "$(gosecrets completion bash)"What gets completed:
| Context | Completes |
|---|---|
gosecrets [TAB] |
init, edit, show, get, version, help, completion |
gosecrets get [TAB] |
credential keys (e.g. database.host, api_key) |
gosecrets --env [TAB] |
environment names from secrets/*.enc files |
gosecrets --root [TAB] |
directory names (filesystem completion) |
gosecrets init --[TAB] |
--env, --root |
gosecrets completion [TAB] |
bash |
All accessors support dot notation for nested keys (e.g. "database.password").
secrets, err := gosecrets.Load()| Option | Description | Example |
|---|---|---|
gosecrets.WithRoot(dir) |
Set root directory for secrets/ |
gosecrets.WithRoot("/app") |
gosecrets.WithEnv(env) |
Set environment name | gosecrets.WithEnv("production") |
// defaults: ./secrets/development.enc
secrets, err := gosecrets.Load()
// custom root + env: ./deploy/secrets/production.enc
secrets, err := gosecrets.Load(
gosecrets.WithRoot("./deploy"),
gosecrets.WithEnv("production"),
)
// Docker/CI absolute path: /app/secrets/production.enc
secrets, err := gosecrets.Load(
gosecrets.WithRoot("/app"),
gosecrets.WithEnv("production"),
)| Method | Return | Zero value | Description |
|---|---|---|---|
Get(key) |
any |
nil |
Raw value |
String(key, fallback...) |
string |
"" |
String representation |
Int(key, fallback...) |
int |
0 |
Integer value |
Int64(key, fallback...) |
int64 |
0 |
64-bit integer value |
Float64(key, fallback...) |
float64 |
0 |
Floating point value |
Bool(key, fallback...) |
bool |
false |
Boolean value |
Duration(key, fallback...) |
time.Duration |
0 |
Parses "5s", "1h30m", etc. |
Map(key, fallback...) |
map[string]any |
nil |
Nested map |
TCPAddr(key, fallback...) |
*net.TCPAddr |
nil |
Parses "host:port" via net.ResolveTCPAddr |
Has(key) |
bool |
false |
Check if key exists |
Keys() |
[]string |
[] |
All dot-notation key paths |
All() |
map[string]any |
— | Entire credentials map |
MustGet(key) |
any |
panic | Like Get, panics if missing |
MustString(key) |
string |
panic | Like String, panics if missing |
MustTCPAddr(key) |
*net.TCPAddr |
panic | Like TCPAddr, panics if missing or invalid |
All accessors (except Get, Has, All, and Must* variants) accept an
optional fallback value. If the key doesn't exist, the fallback is returned
instead of the zero value:
// without fallback — returns zero value when key is missing
host := secrets.String("database.host") // ""
// with fallback — returns fallback when key is missing
host := secrets.String("database.host", "0.0.0.0") // "0.0.0.0"// examples
host := secrets.String("database.host") // "localhost"
port := secrets.Int("database.port") // 5432
pi := secrets.Float64("pi") // 3.14
debug := secrets.Bool("debug") // true
timeout := secrets.Duration("timeout") // 5s
db := secrets.Map("database") // map[string]any{...}
redis := secrets.TCPAddr("redis_addr") // *net.TCPAddr{IP: ..., Port: 6379}
// with fallback values
host = secrets.String("cache.host", "localhost") // "localhost" if missing
port = secrets.Int("cache.port", 6379) // 6379 if missing
timeout = secrets.Duration("cache.ttl", 5*time.Minute) // 5m if missing
redis = secrets.TCPAddr("cache.addr", "localhost:6379") // parsed fallback if missing
// must variants — panic if missing (use at startup)
apiKey := secrets.MustString("api_key") // panics if not found
addr := secrets.MustTCPAddr("redis_addr") // panics if not found or invalid2026-03-25
- Add
--rootflag to all CLI commands (init,edit,show,get) — relative paths only - Add
GOSECRETS_ROOTenvironment variable for absolute paths (CI/CD, Docker) - CLI rejects absolute paths in
--rootflag with a clear error message - Bash completion now supports
--rootwith directory completion - Update documentation with custom root directory examples
2026-03-17
- Add bash completion support (
gosecrets completion bash) - Add
Keys()method for listing all dot-notation key paths - Fix
initnext steps hint to include--envflag when custom environment is used
2026-03-01
- Add optional fallback values to all accessors:
String,Int,Int64,Float64,Bool,Duration,Map - Add
TCPAddr(key, fallback...)method for resolving"host:port"strings to*net.TCPAddr - Add
MustTCPAddr(key)method that panics if key is missing or address is invalid - Add
--version,-v,--help,-hflags to CLI usage output
2026-02-28
- Fix save (edit) bug
v0.4.0 - Initial release
v0.1.0
- Uğur "vigo" Özyılmazel - Creator, maintainer
All PR's are welcome!
fork(https://github.com/bilustek/gosecrets/fork)- Create your
branch(git checkout -b my-feature) commityours (git commit -am 'add some functionality')pushyourbranch(git push origin my-feature)- Than create a new Pull Request!
This project is licensed under MIT
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.