Go config.yaml

Go services often keep both config.yaml and config.yaml.example. A common problem is committing the real config.yaml with production secrets. Choose between reading environment variables and retaining a file supplied securely at runtime according to the service's needs.

When to use this guide

  • The repository contains both config.yaml and config.yaml.example.
  • The Go application loads YAML configuration at startup.
  • Values such as database.password, api.token, or jwt.secret are written directly in config.yaml.

Recommended structure

text
config.yaml.example   # 샘플 설정만 포함, Git 커밋 가능
config.yaml           # 실제 운영값, Git 커밋 금지
.gitignore            # config.yaml 포함

Examples

yaml
server:
  port: 8080

database:
  host: prod-db.internal
  user: app_user
  password: super-secret-password

jwt:
  secret: hardcoded-jwt-secret

Pattern A: Environment-variable injection

This is a simple option for services that can use environment variables.

When to choose this pattern

  • There are relatively few secrets and environment variables are suitable.
  • The deployment system provides environment-variable injection.
  • You do not need to preserve the existing configuration-file structure.

config.yaml.example

yaml
server:
  port: 8080

database:
  host: ${DB_HOST}
  user: ${DB_USER}
  password: ${DB_PASSWORD}

jwt:
  secret: ${JWT_SECRET}

Go code example

This code reads environment variables directly. yaml.Unmarshal does not expand ${ENV_VAR} automatically; a loader that reads the YAML above needs separate substitution logic.

go
package config

import (
	"fmt"
	"os"
)

type Config struct {
	DBPassword string
	JWTSecret  string
}

func Load() (*Config, error) {
	cfg := &Config{
		DBPassword: os.Getenv("DB_PASSWORD"),
		JWTSecret:  os.Getenv("JWT_SECRET"),
	}

	if cfg.DBPassword == "" {
		return nil, fmt.Errorf("DB_PASSWORD is not configured")
	}
	if cfg.JWTSecret == "" {
		return nil, fmt.Errorf("JWT_SECRET is not configured")
	}

	return cfg, nil
}

Pattern B: Inject a mounted config.yaml

If the application must read config.yaml, retain the format but mount the actual file at runtime, for example at /etc/app/config.yaml.

When to choose this pattern

  • The application depends on file-based configuration loading.
  • You need to preserve a hierarchical YAML structure.
  • Kubernetes Secret volumes or secure file delivery are the operational standard.

Repository file: config.yaml.example

yaml
server:
  port: 8080

database:
  host: prod-db.internal
  user: app_user
  password: "<runtime-provided>"

jwt:
  secret: "<runtime-provided>"

Example runtime path

text
/etc/app/config.yaml

Go code example

go
package config

import (
	"fmt"
	"os"

	"gopkg.in/yaml.v3"
)

type FileConfig struct {
	Database struct {
		Password string `yaml:"password"`
	} `yaml:"database"`
	JWT struct {
		Secret string `yaml:"secret"`
	} `yaml:"jwt"`
}

func LoadFromFile(path string) (*FileConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config file: %w", err)
	}

	var cfg FileConfig
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse config file: %w", err)
	}

	if cfg.Database.Password == "" {
		return nil, fmt.Errorf("database.password is not configured")
	}
	if cfg.JWT.Secret == "" {
		return nil, fmt.Errorf("jwt.secret is not configured")
	}

	return &cfg, nil
}

The application must fail immediately if the file is missing or a required secret is empty.

When the Go service must use config.yaml

  • Do not commit the real config.yaml.
  • Keep only config.yaml.example in the repository.
  • Supply the actual file at runtime through a Kubernetes Secret volume, CSI/Vault Agent, or secure file delivery.
  • Do not include production config.yaml in a Docker image.
  • Minimize file permissions and prevent world-readable access.
  • Fail closed when the file or required secrets are missing.
  • Redact secrets from configuration dumps, panics, and debug logs.
  • Document file replacement and reload or restart steps for rotation.

Developer tasks

  • Choose environment variables or a mounted file to suit the service.
  • Make os.Getenv() handling or the file loader fail closed when required secrets are missing.
  • Do not log entire configuration objects.
  • For file-based configuration, document the runtime path and remove hard-coded development secrets.

Infrastructure and platform tasks

  • Configure deployment-time environment-variable injection when using Pattern A.
  • Supply secret files at a standard path such as /etc/app/config.yaml when using Pattern B.
  • Do not distribute production files through Docker images, ConfigMaps, or repository commits.
  • Include rotation and redeployment or reload steps in the operations guide.

Verification

  • Check that config.yaml.example contains no production secrets.
  • Confirm that the actual config.yaml is not tracked by Git.
  • Confirm that a missing file or environment variable causes the application to fail.
  • Check logs, panics, and debug output for secret values.

Common mistakes

  • Committing production secrets in config.yaml for development convenience
  • Mounting configuration but also including the real file in the Docker image
  • Allowing an empty secret so the application starts in an invalid state
  • Storing secrets in a ConfigMap and treating its file mount as secret protection

Remediation steps

  1. Remove actual secrets from config.yaml.
  2. Leave only key names and example formats in config.yaml.example.
  3. Add the real config.yaml to .gitignore. Stop tracking it if it is already tracked; .gitignore does not remove values from previous commits.
  4. For environment variables, read values through os.Getenv() or the configuration loader.
  5. For a mounted file, read it from a runtime path such as /etc/app/config.yaml.
  6. On Kubernetes, choose environment injection or a Secret volume to suit operations.
  7. Treat previous secret values as exposed: revoke them and issue replacements.

Example instructions for the owner

  • “Remove actual secrets from Go's config.yaml and keep only example formats in config.yaml.example.”
  • “Use os.Getenv() when environment variables are suitable. If config.yaml is required, mount the actual file at /etc/app/config.yaml at runtime.”
  • “If config.yaml was committed, treat its secrets as exposed and rotate them immediately.”

Additional checks

  • Have production values been copied into config.yaml.example?
  • Does the loader accept empty secrets?
  • Do debug logs print the entire configuration object?
  • Is production config.yaml included in a Docker image or ConfigMap?

Related documentation