Manage Secrets and Config in Go Apps

Khimananda Oli 7 min read Programming and Languages
Manage Secrets and Config in Go Apps

By Khimananda Oli | Last reviewed: August 2026

Hardcoded credentials and scattered configuration files are the most common security failures I see during infrastructure audits. To properly manage secrets and config in Go apps, you must decouple sensitive data from source code while maintaining a predictable loading hierarchy for different environments. This separation ensures your application remains portable across local development, CI pipelines, and production clusters without exposing critical assets.

Config Loading Precedence (Low → High)Defaults / Codeconfig.yaml FileEnvironment VarsRuntime SecretsHigher layers override lower values automaticallyFinal Merged Configuration StructDB_HOST=prod-db.internal | DB_PORT=5432 | API_KEY=[REDACTED]Validated & Typed → Application Ready
Layered configuration precedence ensures secrets override defaults safely in Go apps

How do you structure configuration loading in Go?

The standard library os.Getenv works for simple scripts but fails for complex applications requiring type safety, validation, and multi-source merging. In practice, most production Go services use a dedicated library like Kubernetes secrets management patterns adapted for application-level code, or more commonly, the Viper library. Viper provides a unified interface to read from files, environment variables, and remote stores while handling case-insensitive key mapping.

Setting up a typed configuration struct

Avoid passing raw maps throughout your codebase. Define a strict struct that represents your entire configuration surface. This enables compile-time checks and makes dependencies explicit.

package config

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Auth     AuthConfig
}

type ServerConfig struct {
    Port int    `mapstructure:"port"`
    Host string `mapstructure:"host"`
}

type DatabaseConfig struct {
    URL      string `mapstructure:"url"`
    MaxConns int    `mapstructure:"max_conns"`
}

type AuthConfig struct {
    JWTSecret string `mapstructure:"jwt_secret"`
}

Loading with Viper and automatic env binding

Bind environment variables automatically so containers can override file-based config without code changes. The prefix prevents collisions in shared environments.

func Load() (*Config, error) {
    v := viper.New()
    v.SetConfigName("config")
    v.SetConfigType("yaml")
    v.AddConfigPath(".")
    v.AddConfigPath("/etc/app/")

    // Bind all keys to APP_ prefixed env vars
    v.SetEnvPrefix("APP")
    v.AutomaticEnv()
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

    // Read file if exists (not required)
    _ = v.ReadInConfig()

    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("unmarshal config: %w", err)
    }
    return &cfg, nil
}

How do you securely inject secrets at runtime?

Configuration files should never contain production secrets. Even encrypted files in Git create operational risk when keys rotate or team members leave. Instead, treat secrets as a distinct concern injected at the platform layer. For teams managing secrets in CI/CD pipelines, this same principle applies: the pipeline provides the secret, the app consumes it blindly.

Environment variables for containerized workloads

Containers make environment variables the universal secret transport. They are supported by every orchestrator and require no additional SDKs. However, they have limitations: visible in process listings, truncated in some logging systems, and difficult to rotate without restarts.

  • Use strong prefixes (APP_DB_PASSWORD) to avoid collisions
  • Never log the full environment block in error handlers
  • Validate presence at startup — fail immediately if missing
  • Consider base64 encoding only if the value contains newlines or special chars

Cloud-native secret stores for production

For AWS, GCP, or Azure deployments, fetch secrets directly from managed stores. This enables rotation without redeployment and centralizes audit trails. If you are building on AWS, refer to managing secrets with AWS Secrets Manager for IAM policy patterns.

import "github.com/aws/aws-sdk-go-v2/service/secretsmanager"

func getSecret(ctx context.Context, name string) (string, error) {
    client := secretsmanager.NewFromConfig(cfg)
    out, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
        SecretId: aws.String(name),
    })
    if err != nil {
        return "", fmt.Errorf("fetch secret %s: %w", name, err)
    }
    return *out.SecretString, nil
}
Secure Secret Injection FlowCloud Secret StoreAWS SM / Vault / GCPEncrypted at RestIAM AuthGo App StartupFetch & ValidatePopulate StructIn-Memory OnlyNo Disk WriteZero Log Exposure⚠ Never cache secrets to disk or include in stack traces
Secrets flow directly from managed store to application memory without intermediate storage

How do you validate configuration at startup?

Failing after 30 minutes of operation because a typo broke a database URL is unacceptable. Validate every required field during initialization. Use a validation library or custom methods on your config struct to enforce constraints before any HTTP server starts or database connection opens.

func (c *Config) Validate() error {
    if c.Server.Port <= 0 || c.Server.Port > 65535 {
        return errors.New("server.port must be between 1 and 65535")
    }
    if c.Database.URL == "" {
        return errors.New("database.url is required")
    }
    if len(c.Auth.JWTSecret) < 32 {
        return errors.New("auth.jwt_secret must be at least 32 characters")
    }
    return nil
}

// In main.go
cfg, err := config.Load()
if err != nil { log.Fatal(err) }
if err := cfg.Validate(); err != nil {
    log.Fatalf("invalid configuration: %v", err)
}

This pattern aligns with Twelve-Factor App principles where config validity is a deployment-time concern, not a runtime surprise. For observability integration, ensure validated config values (non-secret ones) are exposed via health endpoints or structured logs as described in structured logging best practices.

What are the trade-offs between config approaches in Go?

No single method fits every scenario. Small CLI tools differ fundamentally from long-lived microservices. Understanding these trade-offs prevents over-engineering simple tools or under-securing critical services.

ApproachBest ForSecurity LevelRotation SupportComplexity
Hardcoded ConstantsPrototypes onlyCritical RiskNoneTrivial
.env Files + godotenvLocal DevelopmentLowManual RestartLow
Viper + YAML + Env OverrideMost Production AppsMedium-HighRestart RequiredMedium
Cloud Secret Manager SDKRegulated / Multi-TenantHighDynamic PossibleMedium-High
Kubernetes Secrets VolumeK8s-Native WorkloadsHighPod RestartPlatform Dependent
HashiCorp Vault AgentEnterprise / Zero TrustHighestAutomatic RenewalHigh

In my experience auditing Nepal-based fintech startups and global SaaS platforms alike, the Viper + Environment Variable hybrid covers 80% of use cases. Reserve Vault or direct SDK integration for systems handling PII, payments, or requiring SOC 2 compliance evidence trails.

How do you prevent secret leaks in Go projects?

Technical controls matter less than workflow discipline. Even perfect encryption fails if a developer copies a production key into a test file. Implement defense-in-depth across the entire lifecycle.

  1. Add pre-commit hooks: Tools like gitleaks or trufflehog scan staged changes before they reach remote repositories. Configure them in .pre-commit-config.yaml so enforcement is automatic.
  2. Exclude config files from version control: Your .gitignore must include *.env, config.prod.yaml, and any private key patterns. Commit only config.example.yaml with placeholder values.
  3. Redact logs aggressively: Create a custom zap/slog encoder that masks fields named password, secret, token, or key. Assume every log line will eventually appear in a support ticket.
  4. Rotate on personnel changes: When an engineer leaves or changes roles, assume their cached credentials are compromised. Automate rotation where possible; document manual steps otherwise.
  5. Audit access patterns: Cloud secret stores provide access logs. Set alerts for unusual fetch frequencies or source IPs outside your VPC/Kubernetes cluster.
Secret Leak Prevention LayersPre-Commit Scangitleaks blocks leaks locallyCI Pipeline GateFail build on detected secretsRuntime GuardLog redaction + IAM scopeHuman Layer: Training + Access Reviews + Rotation PolicyTechnical controls fail without organizational disciplineEach layer catches what the previous missed
Multi-layered prevention strategy combines automated scanning with human process controls

Manage Secrets and Config in Go Apps for Production Readiness

Getting configuration right is foundational to reliable operations. Start with Viper and environment variables for flexibility, add cloud secret stores when compliance demands it, and validate everything before your first request handler runs. Remember that the goal isn't just security — it's predictable behavior across every environment your team touches. If your current setup involves committed YAML files with real passwords or unvalidated startup sequences, prioritize fixing those gaps this week. For architecture reviews or help implementing secure configuration patterns in your Go services, reach out to discuss your specific requirements.

Frequently Asked Questions

Viper remains the standard for configuration management in 2026, while Infisical or AWS Secrets Manager handle sensitive credentials. Avoid rolling custom encryption; use established libraries that support environment variable overrides and remote secret backends natively.

Use os.Getenv with explicit defaults and validation at startup. Libraries like caarlos0/env parse structs directly from environment variables, providing type safety and failing fast if required secrets are missing during application initialization.

Never commit .env files containing real secrets. Commit only .env.example with placeholder values to document required variables. Add .env to .gitignore immediately to prevent accidental credential leaks in Git history.

Implement a signal handler for SIGHUP to reload configuration dynamically. For managed services like HashiCorp Vault, use the agent sidecar pattern to fetch updated secrets automatically without requiring application restarts or downtime.

Config includes non-sensitive settings like ports and feature flags. Secrets are credentials, API keys, and certificates requiring encryption at rest and strict access controls. Store them separately using dedicated secret managers rather than plain config files.

Define a config struct with validation tags using go-playground/validator. Parse all sources into this struct during init and return descriptive errors immediately if required fields are missing or malformed, preventing runtime failures later.

Yes, mount Kubernetes secrets as volume files or inject them as environment variables via pod specs. Your Go app reads them using standard file or env APIs without needing Kubernetes-specific SDKs, keeping code portable across environments.

Use environment-specific prefixes or separate config files loaded conditionally based on an APP_ENV variable. Viper supports merging base configs with environment overlays, ensuring dev, staging, and production settings stay isolated yet consistent.

Never log raw secret values. Redact sensitive fields before logging by implementing custom Stringer interfaces or using structured logging libraries that support field masking. Audit logs regularly to ensure no credentials leak through debug output.

Inject configuration via interfaces rather than reading globals directly. Use testify/mock or manual fakes to provide test credentials during unit tests. Integration tests should use ephemeral secret stores or dockerized vault instances.

YAML is preferred for human readability and comments. TOML works well for simple key-value structures. Avoid JSON for primary config since it lacks comment support. Always allow environment variable overrides regardless of file format chosen.

Use cloud-native KMS solutions like AWS KMS or GCP Cloud KMS. For local development, use SOPS with age encryption. Never store plaintext secrets in databases or config files; always encrypt before persisting to disk.

Viper integrates with etcd, Consul, and Firebase Remote Config directly. For Vault or AWS Secrets Manager, use dedicated providers or wrapper libraries. Fetch remote secrets once at startup and cache locally to reduce latency.

Always read from environment variables, mounted files, or secret managers. Use linting tools like gitleaks or trufflehog in CI pipelines to scan commits for hardcoded credentials before they reach main branches.

Fail fast during initialization with a clear error message naming the missing variable. Do not use empty defaults for critical secrets. This prevents subtle bugs where applications run partially broken instead of crashing visibly at startup.