Skip to content

CWE-798: Use of Hard-coded Credentials - Go

Overview

Hard-coded credentials vulnerabilities in Go applications occur when sensitive authentication credentials (passwords, API keys, cryptographic keys, database connection strings, tokens) are embedded directly in source code, configuration files committed to version control, or compiled binaries. These credentials become accessible to anyone with access to the codebase, version history, or binary - including attackers who obtain source code through repository breaches, insider threats, decompilation, or public code exposure.

The risks are severe: hard-coded database passwords grant unauthorized database access, allowing data theft or modification. Hard-coded API keys enable attackers to abuse external services at the victim's expense. Hard-coded encryption keys allow decryption of supposedly encrypted data. Hard-coded admin passwords provide full system access. Once credentials are committed to version control (Git), they persist in history indefinitely - even if later removed, they remain in old commits accessible to anyone who clones the repository.

Common patterns include hardcoded strings in connection strings, encryption keys as byte slices in code, API keys in constants, default passwords that aren't changed, and credentials in configuration files tracked by version control. Go applications often commit .env files, config.yaml files, or database initialization scripts containing credentials. Compiled binaries contain string literals that can be extracted with simple tools like strings command.

Primary Defence: Store credentials in environment variables, secret management systems (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault), or configuration files excluded from version control (.gitignore). Use placeholder values in tracked config files, with real values injected at deployment. Rotate credentials regularly. Never commit credentials to version control.

Common Vulnerable Patterns

Hard-coded Database Password

// VULNERABLE - Database credentials in source code
package main

import (
    "database/sql"
    "fmt"

    _ "github.com/lib/pq"
)

func connectToDatabase() (*sql.DB, error) {
    // DANGEROUS: Hard-coded credentials
    connStr := "host=db.example.com port=5432 user=admin password=SuperSecret123! dbname=production sslmode=disable"

    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, err
    }

    return db, nil
}

// VULNERABILITY:
// 1. Password visible in source code
// 2. Exposed in version control history
// 3. Visible in compiled binary (strings command)
// 4. Shared with all developers who have repository access
// 5. Difficult to rotate without code changes

Why this is vulnerable: The password SuperSecret123! is embedded directly in the code. Anyone with repository access sees it. When committed to Git, the password lives forever in history - even if changed in later commits, git log and git blame reveal it. Compiled binaries contain the string literal, extractable with strings ./binary | grep password. Developers might share the code publicly, upload to GitHub by mistake, or be targeted by attackers seeking credentials. Rotating the password requires code changes, build, and deployment - a slow process during security incidents.

Hard-coded API Keys

// VULNERABLE - API keys as constants
package main

import (
    "fmt"
    "net/http"
)

const (
    // DANGEROUS: API keys in source code
    StripeAPIKey   = "sk_live_51234567890abcdefghijklmnop"
    AWSAccessKey   = "AKIAIOSFODNN7EXAMPLE"
    AWSSecretKey   = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
)

func chargeCustomer(amount int, token string) error {
    req, _ := http.NewRequest("POST", "https://api.stripe.com/v1/charges", nil)
    req.Header.Set("Authorization", "Bearer "+StripeAPIKey)

    // Make request...
    return nil
}

// RISKS:
// - Keys leaked to any repository viewer
// - Keys visible in compiled binaries
// - Cannot rotate without redeployment
// - Accidental public repository pushes expose keys
// - Stripe charges, AWS resources created by attackers

Why this is vulnerable: API keys grant access to paid services. Exposed Stripe keys allow attackers to create fraudulent charges or refunds. AWS keys provide full cloud access - attackers can spin up expensive resources, access data, or pivot to other systems. Declaring keys as constants makes them prominently visible in code and easy to grep for (grep -r "sk_live" .). Public repository scanners automatically detect and report API keys (GitHub secret scanning), often resulting in immediate key revocation, breaking production. Services like Stripe and AWS prevent reuse of exposed keys.

Encryption Keys in Code

// VULNERABLE - Hard-coded encryption key
import (
    "crypto/aes"
    "crypto/cipher"
)

var (
    // DANGEROUS: Encryption key in source code
    encryptionKey = []byte("32-byte-aes-key-for-encryption!!")
)

func encryptData(plaintext []byte) ([]byte, error) {
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    // Encryption logic...
    return nil, nil
}

// VULNERABILITY:
// Anyone with code access can decrypt all encrypted data
// Key rotation requires code changes
// Historical commits reveal old keys that may still decrypt old data

Why this is vulnerable: Encryption provides no security if the key is public. Database encryption, file encryption, or token encryption become worthless when the key is in source code. Attackers who gain read-only repository access can decrypt all data. The key persists in version history forever. If different branches use different keys, managing which key decrypts which data becomes complex. Key rotation requires coordinated code deployment and data re-encryption, often prohibitively expensive. Encryption keys should be treated like passwords - stored in secure key management systems, never in code.

Configuration Files with Credentials in Git

// config.yaml - VULNERABLE (committed to Git)
database:
  host: localhost
  port: 5432
  username: admin
  password: ProductionPassword123  # DANGEROUS

aws:
  access_key: AKIAIOSFODNN7EXAMPLE  # DANGEROUS
  secret_key: wJalrXUtnFEMI/K7MDENG  # DANGEROUS

// Code that loads config
package main

import (
    "gopkg.in/yaml.v3"
    "os"
)

type Config struct {
    Database struct {
        Host     string `yaml:"host"`
        Port     int    `yaml:"port"`
        Username string `yaml:"username"`
        Password string `yaml:"password"`
    } `yaml:"database"`
}

func loadConfig() (*Config, error) {
    data, err := os.ReadFile("config.yaml")
    if err != nil {
        return nil, err
    }

    var config Config
    err = yaml.Unmarshal(data, &config)
    return &config, err
}

// VULNERABILITY:
// config.yaml tracked in Git
// Contains production credentials
// Visible to all developers and in version history

Why this is vulnerable: Configuration files tracked by version control inherit all the same risks as hard-coded credentials. Developers often commit config.yaml, .env, or database.yml files containing production credentials during initial development, forgetting to remove them before committing. Git history preserves these files even if later deleted. The .gitignore file must exclude credential-containing files from the start - adding files to .gitignore after they've been committed doesn't remove them from history.

Default Credentials Not Changed

// VULNERABLE - Default credentials
package main

import (
    "fmt"
    "net/http"
)

var (
    // DANGEROUS: Default admin credentials
    defaultAdminUser = "admin"
    defaultAdminPass = "admin123"
)

func loginHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    // Check against default credentials
    if username == defaultAdminUser && password == defaultAdminPass {
        // Grant admin access
        grantAdminSession(w)
        return
    }

    // Check database for other users
    checkDatabase(username, password)
}

// RISK:
// Default credentials are public knowledge
// Attackers try "admin/admin123" first on every deployment
// Often forgotten in production systems

Why this is vulnerable: Default credentials are well-known and widely shared. Attackers scan for services using default passwords (admin/admin, admin/password, root/toor). Many data breaches result from unchanged default credentials on databases, admin panels, or IoT devices. Even if intended only for initial setup, default credentials often remain active in production. Security scanners and penetration testers check for default credentials in the first minutes of assessment. Systems should force credential changes on first use and prevent setting credentials to known defaults.

Secure Patterns

Environment Variables for Credentials

// SECURE - Loading credentials from environment variables
package main

import (
    "database/sql"
    "fmt"
    "os"

    _ "github.com/lib/pq"
)

func connectToDatabase() (*sql.DB, error) {
    // SECURE: Read credentials from environment
    host := os.Getenv("DB_HOST")
    port := os.Getenv("DB_PORT")
    user := os.Getenv("DB_USER")
    password := os.Getenv("DB_PASSWORD")
    dbname := os.Getenv("DB_NAME")

    // Validate required environment variables
    if host == "" || user == "" || password == "" {
        return nil, fmt.Errorf("missing required database environment variables")
    }

    // Set defaults for optional variables
    if port == "" {
        port = "5432"
    }
    if dbname == "" {
        dbname = "production"
    }

    // Build connection string
    connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=require",
        host, port, user, password, dbname)

    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, err
    }

    return db, nil
}

// Deploy with environment variables:
// export DB_HOST=db.example.com
// export DB_USER=app_user
// export DB_PASSWORD=ActualProductionPassword
// export DB_NAME=production
// ./myapp

Why this works: Environment variables separate credentials from code. Different environments (development, staging, production) have different credentials without code changes. Environment variables aren't committed to version control. Deployment systems (Docker, Kubernetes, systemd) securely inject environment variables at runtime. Access to source code doesn't grant access to credentials. Rotating credentials only requires updating environment variables and restarting the application, no code changes. This is the 12-factor app methodology's recommended approach.

Configuration Files Excluded from Version Control

// config.yaml.example - SECURE (tracked in Git)
database:
  host: localhost
  port: 5432
  username: YOUR_DB_USERNAME
  password: YOUR_DB_PASSWORD

aws:
  access_key: YOUR_AWS_ACCESS_KEY
  secret_key: YOUR_AWS_SECRET_KEY

// config.yaml - SECURE (NOT tracked - in .gitignore)
database:
  host: db.prod.example.com
  port: 5432
  username: prod_user
  password: ActualProductionPassword

aws:
  access_key: AKIAIOSFODNN7REALKEY
  secret_key: RealSecretKeyNotInGit

// .gitignore
config.yaml
.env
secrets/
*.key
*.pem

// Code
package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "os"
)

type Config struct {
    Database struct {
        Host     string `yaml:"host"`
        Port     int    `yaml:"port"`
        Username string `yaml:"username"`
        Password string `yaml:"password"`
    } `yaml:"database"`
}

func loadConfig(path string) (*Config, error) {
    // SECURE: Load credentials from untracked config file
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("config file not found: %w\nCopy config.yaml.example to config.yaml and configure", err)
    }

    var config Config
    if err := yaml.Unmarshal(data, &config); err != nil {
        return nil, err
    }

    // Validate required fields
    if config.Database.Password == "YOUR_DB_PASSWORD" {
        return nil, fmt.Errorf("config.yaml contains placeholder values - please configure real credentials")
    }

    return &config, nil
}

func main() {
    config, err := loadConfig("config.yaml")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }

    // Use config...
}

Why this works: config.yaml.example provides template documentation tracked in Git, showing developers what configuration is needed without exposing real credentials. .gitignore prevents config.yaml (containing real credentials) from being committed. Developers copy the example and fill in their credentials locally. Each environment (dev, staging, prod) has its own config.yaml with appropriate credentials. The validation check prevents deploying with placeholder values. Deployment documentation instructs operators to create config.yaml with real credentials during deployment.

Secret Management Systems

// SECURE - Using AWS Secrets Manager
package main

import (
    "context"
    "encoding/json"
    "fmt"

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

type DatabaseCredentials struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Username string `json:"username"`
    Password string `json:"password"`
    DBName   string `json:"dbname"`
}

func getDBCredentialsFromSecretsManager(ctx context.Context, secretName string) (*DatabaseCredentials, error) {
    // SECURE: Load from AWS Secrets Manager
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return nil, err
    }

    client := secretsmanager.NewFromConfig(cfg)

    result, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
        SecretId: &secretName,
    })
    if err != nil {
        return nil, fmt.Errorf("failed to retrieve secret: %w", err)
    }

    var creds DatabaseCredentials
    if err := json.Unmarshal([]byte(*result.SecretString), &creds); err != nil {
        return nil, err
    }

    return &creds, nil
}

func connectWithSecretsManager(ctx context.Context) error {
    // Retrieve credentials from Secrets Manager
    creds, err := getDBCredentialsFromSecretsManager(ctx, "prod/database/credentials")
    if err != nil {
        return err
    }

    // Use credentials
    connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
        creds.Host, creds.Port, creds.Username, creds.Password, creds.DBName)

    // Connect to database...
    fmt.Println("Connected with SecureString from Secrets Manager")
    return nil
}

Why this works: AWS Secrets Manager (and similar services: HashiCorp Vault, Azure Key Vault, Google Secret Manager) centralize secret storage with strong access controls, audit logging, automatic rotation, and encryption at rest. Credentials are retrieved at application startup or periodically refreshed. IAM roles/policies control which applications can access which secrets. Credential rotation doesn't require application redeployment - just restart to fetch new credentials. Audit logs track all secret access. Secrets never exist in code or version control. This is the enterprise-grade approach for production systems.

HashiCorp Vault Integration

// SECURE - Using HashiCorp Vault
import (
    "context"
    "fmt"

    vault "github.com/hashicorp/vault/api"
)

func getSecretFromVault(secretPath string) (map[string]interface{}, error) {
    // SECURE: Vault configuration from environment
    config := vault.DefaultConfig()
    config.Address = os.Getenv("VAULT_ADDR") // e.g., https://vault.example.com

    client, err := vault.NewClient(config)
    if err != nil {
        return nil, err
    }

    // Authenticate with token (in production, use AppRole or cloud auth)
    client.SetToken(os.Getenv("VAULT_TOKEN"))

    // Read secret
    secret, err := client.Logical().Read(secretPath)
    if err != nil {
        return nil, err
    }

    if secret == nil || secret.Data == nil {
        return nil, fmt.Errorf("secret not found")
    }

    return secret.Data, nil
}

func getAPIKey(keyName string) (string, error) {
    secretPath := fmt.Sprintf("secret/data/api-keys/%s", keyName)

    data, err := getSecretFromVault(secretPath)
    if err != nil {
        return "", err
    }

    // Vault v2 secrets have nested data structure
    dataField, ok := data["data"].(map[string]interface{})
    if !ok {
        return "", fmt.Errorf("invalid secret format")
    }

    apiKey, ok := dataField["api_key"].(string)
    if !ok {
        return "", fmt.Errorf("api_key not found in secret")
    }

    return apiKey, nil
}

Why this works: Vault provides dynamic secrets (generated on-demand), automatic rotation, fine-grained access control, and comprehensive audit logging. Applications authenticate using tokens, AppRole (application identity), or cloud provider IAM. Secrets are fetched at runtime, never stored in code. Vault encrypts secrets at rest and in transit. Access policies control which applications can read which secrets. The audit log records all secret access for compliance and security monitoring. This is ideal for multi-cloud deployments and complex credential management.

Encrypted Configuration with KMS

// SECURE - Encrypt sensitive config values with AWS KMS
import (
    "context"
    "encoding/base64"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/kms"
)

func encryptConfigValue(ctx context.Context, plaintext, keyID string) (string, error) {
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return "", err
    }

    client := kms.NewFromConfig(cfg)

    result, err := client.Encrypt(ctx, &kms.EncryptInput{
        KeyId:     &keyID,
        Plaintext: []byte(plaintext),
    })
    if err != nil {
        return "", err
    }

    // Return base64-encoded ciphertext
    return base64.StdEncoding.EncodeToString(result.CiphertextBlob), nil
}

func decryptConfigValue(ctx context.Context, ciphertext string) (string, error) {
    ciphertextBlob, err := base64.StdEncoding.DecodeString(ciphertext)
    if err != nil {
        return "", err
    }

    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return "", err
    }

    client := kms.NewFromConfig(cfg)

    result, err := client.Decrypt(ctx, &kms.DecryptInput{
        CiphertextBlob: ciphertextBlob,
    })
    if err != nil {
        return "", err
    }

    return string(result.Plaintext), nil
}

// config.yaml can now contain encrypted values:
// database:
//   password: "AQICAHi...encrypted-base64-string..."
//
// Application decrypts at startup using KMS

Why this works: KMS encrypts configuration values, allowing config files to be committed to version control (encrypted ciphertext visible, but plaintext protected). Only applications/users with KMS decrypt permission can read values. KMS handles key management, rotation, and audit logging. The encryption key never leaves KMS - it's not exposed to applications. This balances version control tracking (config structure visible) with security (sensitive values encrypted). Access control is managed via IAM policies. This works well for compliance requirements mandating encryption of credentials at rest.

Additional Resources