GSY is a lightweight Go library for secure secret management in YAML configuration files. It provides a SecretString type that implements YAML unmarshaling, allowing you to use the same config files across local development (plain strings) and production (Google Cloud Secret Manager references) without code changes.
The library checks a global environment mode (local or prod) to decide how to resolve secrets during unmarshaling. This makes it ideal for Go applications using YAML configs (e.g., with gopkg.in/yaml.v3).
- Seamless Environment Switching: Use identical YAML files in dev and prod. Set
SetEnv("local")for plain strings,SetEnv("prod")for GCP fetches – no config branching or rebuilds. - Enhanced Security: Secrets are never hardcoded in code or exposed in env vars. Production secrets stay securely in GCP Secret Manager, accessed on-demand.
- Simplicity & Integration: Embed
SecretStringdirectly in your config structs. Works out-of-the-box with standard YAML unmarshaling; minimal boilerplate. - Lightweight & Fast: Minimal dependencies (only
yaml.v3and GCP client). No runtime overhead in local mode. - Flexible: Easy to extend for other secret providers (e.g., AWS SSM) by forking the unmarshaler logic.
GSY solves the common pain of managing secrets in microservices or CLI tools where configs are YAML-based, ensuring compliance with security best practices like least privilege access.
Add GSY to your go.mod:
go get github.com/dvormagic/gsy/secretstringFor production use with GCP, ensure your environment has Google Cloud authentication set up (e.g., via Application Default Credentials or service account keys).
The core of GSY is its support for multi-environment YAML files. Create separate configs like config.local.yml (plain strings for local dev) and config.yml (GCP secret references for prod). Load the appropriate file based on ENV, set SetEnv(env), and unmarshal – secrets resolve automatically.
config.local.yml (plain strings, no maps needed):
api_key: "sk-123-local-api-key" # Resolved directly
database_url: "postgresql://user:local-pass@localhost/db" # Plain full URL
other_key: "regular-value"config.yml (GCP references for prod):
api_key:
secret: "projects/my-project/secrets/api-key/versions/latest" # Fetched from GCP
database_url:
secret: "projects/my-project/secrets/db-url/versions/latest" # Fetched from GCP
other_key: "regular-value"package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
"github.com/dvormagic/gsy/secretstring"
)
type Config struct {
APIKey secretstring.SecretString `yaml:"api_key"`
Database secretstring.SecretString `yaml:"database_url"`
OtherKey string `yaml:"other_key"`
}
func main() {
env := os.Getenv("ENV") // e.g., "local" or "prod"
secretstring.SetEnv(env)
var filename string
if env == "local" {
filename = "config.local.yml"
} else {
filename = "config.yml"
}
data, err := os.ReadFile(filename)
if err != nil {
log.Fatal("Failed to read config:", err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
log.Fatal(err)
}
fmt.Printf("API Key: %s\n", string(cfg.APIKey))
fmt.Printf("DB URL: %s\n", string(cfg.Database))
fmt.Printf("Other: %s\n", cfg.OtherKey)
}Local Mode (ENV=local, loads config.local.yml):
API Key: sk-123-local-api-key
DB URL: postgresql://user:local-pass@localhost/db
Other: regular-value
Prod Mode (ENV=prod, loads config.yml, simulates GCP fetches):
API Key: x78xdfhawo487arhf2 # Mock GCP secret for api-key
DB URL: postgresql://user:x9f3k8m2p7q4r5t1@prod-db-host/db # Mock GCP secret for db-url (full resolved URL)
Other: regular-value
This demonstrates the key benefit: same struct, different files/modes, automatic resolution – plain in local, fetched in prod.
[Keep existing Multi-Environment section as is, since it's complementary]
- Plain String (Local/Prod fallback):
api_key: "my-secret-value" - GCP Secret Reference (Prod only):
api_key: secret: "projects/my-project/secrets/my-api-key/versions/latest"
secretNamemust be the full GCP resource name.- In local mode, it falls back to using the reference as a plain string (useful for mocking).
graph LR
A[YAML Config Load] --> B{SetEnv called?}
B -->|No| C[Default: local mode]
B -->|Yes| D[Mode: local or prod]
D --> E[Unmarshal SecretString field]
E --> F{Plain string?}
F -->|Yes| G[Assign directly]
F -->|No| H{Map with 'secret' key?}
H -->|Yes & prod| I[Fetch from GCP Secret Manager]
H -->|No| J[Error: Invalid format]
I --> K[Assign fetched value]
G --> L[Config ready]
K --> L
J --> M[Unmarshal fails]
- Ensure your Go app runs in an environment with GCP credentials (e.g., GCE, GKE, or
GOOGLE_APPLICATION_CREDENTIALSenv var). - Secret names follow GCP format:
projects/{project}/secrets/{secret}/versions/latest. - Handle errors from
UnmarshalYAMLorFetchSecretFromGCPfor IAM/permission issues.
if err := yaml.Unmarshal(data, &cfg); err != nil {
// Could be GCP access error in prod
log.Printf("Config error: %v", err)
// Fallback: Use env vars or defaults
}The library uses a simple string check. Extend by modifying secretStringEnv logic or subclassing SecretString.
Use FetchSecretFromGCP independently:
val, err := secretstring.FetchSecretFromGCP("projects/my-project/secrets/my-secret/versions/latest")
if err != nil {
// Handle error
}See the example/main.go for a runnable demo with both modes and error handling.
Run unit tests (add them via go test):
go test ./secretstringIntegration tests with GCP require a test project and secrets.
- Fork the repo.
- Create a feature branch (
git checkout -b feature/amazing-feature). - Commit changes (
git commit -m 'Add amazing feature'). - Push (
git push origin feature/amazing-feature). - Open a Pull Request.
Please add tests for new features and ensure code passes go fmt and go vet.
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by secure config patterns in Go microservices. Thanks to Google Cloud for Secret Manager.
Built with ❤️ for secure Go configs.