Skip to content

Secrets & ConfigMaps

Learn how to securely manage secrets and configuration in tsops.

Secret Management

tsops provides powerful secret management with automatic validation.

Tip: Root-level secret/configMap definitions run during Node execution, so read environment variables with process.env. The env() helper is only available inside app-level env functions.

Basic Usage

Define secrets at the config root level (not within apps):

typescript
export default defineConfig({
  project: 'my-app',
  
  namespaces: {
    dev: { production: false },
    prod: { production: true }
  },
  
  // Secrets are defined at config root level
  secrets: {
    'api-secrets': ({ production }) => ({
      JWT_SECRET: production
        ? process.env.JWT_SECRET ?? ''
        : 'dev-jwt',
      DB_PASSWORD: production
        ? process.env.DB_PASSWORD ?? ''
        : 'dev-password',
      API_KEY: production
        ? process.env.API_KEY ?? ''
        : 'dev-key'
    })
  },
  
  apps: {
    api: {
      // Reference secrets in env using secret() helper
      env: ({ secret }) => ({
        JWT_SECRET: secret('api-secrets', 'JWT_SECRET'),
        DB_PASSWORD: secret('api-secrets', 'DB_PASSWORD')
      })
    }
  }
})

envFrom: Reference Entire Secret

Use secret() helper to inject all keys from a secret:

typescript
// Define secrets at config root level
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.JWT_SECRET ?? ''
      : 'dev-jwt',
    DB_PASSWORD: production
      ? process.env.DB_PASSWORD ?? ''
      : 'dev-password'
  })
},

apps: {
  api: {
    // Reference entire secret as envFrom
    env: ({ secret }) => secret('api-secrets')  // ← envFrom: secretRef
  }
}

This generates:

yaml
envFrom:
  - secretRef:
      name: api-secrets

valueFrom: Reference Specific Keys

Mix static values with secret references:

typescript
apps: {
  api: {
    env: ({ secret }) => ({
      PORT: '3000',                                    // Static
      JWT_SECRET: secret('api-secrets', 'JWT_SECRET'),  // From secret
      DB_PASSWORD: secret('api-secrets', 'DB_PASSWORD') // From secret
    })
  }
}

This generates:

yaml
env:
  - name: PORT
    value: "3000"
  - name: JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: JWT_SECRET
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: api-secrets
        key: DB_PASSWORD

Secret Validation

tsops automatically validates secrets before deployment.

What's Validated

✅ No undefined values
✅ No placeholder values (change-me, replace-me, todo, fixme)
✅ No references to missing environment variables

Example: Missing Values

typescript
secrets: {
  'api-secrets': {
    JWT_SECRET: process.env.PROD_JWT,  // ← undefined!
    DB_PASSWORD: 'change-me'            // ← placeholder!
  }
}

tsops will show:

❌ Secret "api-secrets" for app "api" contains missing or placeholder values.

Missing/placeholder keys:
  - JWT_SECRET = "undefined"
  - DB_PASSWORD = "change-me"

Secret does not exist in cluster (namespace: "production").

Please provide actual values by:
  1. Setting environment variables before deployment
  2. Updating your tsops.config.ts with real values
  3. Creating the secret manually in the cluster first

Fallback to Cluster Secrets

If validation fails, tsops checks if the secret exists in the cluster:

bash
kubectl get secret api-secrets -n production

If found, tsops uses the existing secret. This allows:

  • ✅ Manual secret creation
  • ✅ Secret rotation without config changes
  • ✅ Different secrets per environment

ConfigMaps

Similar to secrets, but for non-sensitive configuration.

Basic Usage

ConfigMaps are defined at config root level, similar to secrets:

typescript
export default defineConfig({
  // ConfigMaps at root level
  configMaps: {
    'api-config': {
      LOG_LEVEL: 'info',
      MAX_CONNECTIONS: '100',
      FEATURE_FLAGS: 'auth,payments,notifications'
    }
  },
  
  apps: {
    api: {
      // Reference in env
      env: ({ configMap }) => ({
        LOG_LEVEL: configMap('api-config', 'LOG_LEVEL')
      })
    }
  }
})

envFrom: Reference Entire ConfigMap

typescript
// Define at root level
configMaps: {
  'api-config': {
    LOG_LEVEL: 'info',
    FEATURE_FLAGS: 'auth,payments'
  }
},

apps: {
  api: {
    // Reference entire configMap as envFrom
    env: ({ configMap }) => configMap('api-config')
  }
}

valueFrom: Reference Specific Keys

typescript
apps: {
  api: {
    env: ({ configMap }) => ({
      LOG_LEVEL: configMap('api-config', 'LOG_LEVEL'),
      MAX_CONNECTIONS: configMap('api-config', 'MAX_CONNECTIONS')
    })
  }
}

Best Practices

✅ Use environment variables for secrets

typescript
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.PROD_JWT ?? ''
      : 'dev-jwt'
  })
}

Then deploy with:

bash
PROD_JWT=xxx PROD_DB_PWD=yyy pnpm tsops deploy --namespace prod

✅ Different secrets per environment

typescript
secrets: {
  'api-secrets': ({ production }) => ({
    JWT_SECRET: production
      ? process.env.JWT_SECRET ?? ''
      : 'dev-jwt-secret',
    DB_PASSWORD: production
      ? process.env.DB_PASSWORD ?? ''
      : 'dev-password'
  })
}

✅ Use template for external database URLs

typescript
secrets: {
  'api-secrets': ({ template, env }) => ({
    // For EXTERNAL databases with credentials:
    DATABASE_URL: template('postgresql://{user}:{pwd}@{host}:{port}/{db}', {
      user: 'myuser',
      pwd: env('DB_PASSWORD', ''),
      host: env('DB_HOST', 'external-db.example.com'),  // External host
      port: '5432',
      db: 'myapp'
    })
  })
}

// ⚠️ For INTERNAL services, use runtime config in your app code:
// import config from './tsops.config'
// const POSTGRES_URL = config.url('postgres', 'service')

❌ Don't hardcode secrets

typescript
secrets: {
  'api-secrets': {
    JWT_SECRET: 'hardcoded-secret'  // ❌ Never do this!
  }
}

❌ Don't commit secrets to git

bash
# .gitignore
.env
.env.local
.env.production

Runtime Access

Access resolved environment variables at runtime directly from your config object:

typescript
// server.ts
import config from './tsops.config'

// Set TSOPS_NAMESPACE to determine which namespace to use
process.env.TSOPS_NAMESPACE = 'prod'

// Get resolved environment for an app
const dbPassword = config.env('api', 'DB_PASSWORD')
console.log('JWT_SECRET:', config.env('api', 'JWT_SECRET'))
console.log('DB_PASSWORD:', dbPassword)

// Or get URLs directly
console.log('Internal endpoint:', config.url('api', 'cluster'))
console.log('External endpoint:', config.url('api', 'ingress'))

This provides:

  • ✅ Single source of truth
  • ✅ Type-safe environment access
  • ✅ Works in dev and production
  • ✅ No duplication
  • ✅ Built-in - no extra packages needed

Security Checklist

  • [ ] All production secrets from environment variables
  • [ ] No secrets in git
  • [ ] .env files in .gitignore
  • [ ] Different secrets per environment
  • [ ] Secret rotation plan
  • [ ] Access controls on secrets
  • [ ] Audit logging enabled

Next Steps

Released under the MIT License.