Skip to main content

Overview

The GovTech platform uses AWS Secrets Manager for centralized, encrypted credential storage with automatic rotation and audit trails.
Never Hardcode Secrets:WRONG:
const dbPassword = 'my-secret-password';  // NEVER DO THIS
const apiKey = process.env.API_KEY;  // Environment variables can leak
CORRECT:
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

const secret = await secretsManager
  .getSecretValue({ SecretId: 'govtech/prod/db-credentials' })
  .promise();

const dbCreds = JSON.parse(secret.SecretString);

Secret Storage Architecture

┌─────────────────────────────────────────────────────────────┐
│                    AWS Secrets Manager                       │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  govtech/prod/db-credentials  (KMS Encrypted)         │  │
│  │  govtech/prod/jwt-secret      (KMS Encrypted)         │  │
│  │  govtech/staging/db-credentials                       │  │
│  │  govtech/dev/db-credentials                           │  │
│  │  govtech/cicd/github-token                            │  │
│  └───────────────────────────────────────────────────────┘  │
└────────────────────────┬────────────────────────────────────┘

                         │ IRSA (IAM Role for Service Account)
                         │ No hardcoded credentials needed

              ┌──────────────────────┐
              │  Backend Pod         │
              │  ServiceAccount:     │
              │  govtech-backend     │
              │                      │
              │  Annotation:         │
              │  eks.amazonaws.com/  │
              │  role-arn: arn:aws:  │
              │  iam::835960996869:  │
              │  role/govtech-       │
              │  backend-role        │
              └──────────────────────┘

AWS Secrets Manager

Secret Naming Convention

Secrets follow a hierarchical path structure:
{project}/{environment}/{secret-type}
Examples:
  • govtech/prod/db-credentials
  • govtech/prod/jwt-secret
  • govtech/staging/db-credentials
  • govtech/dev/db-credentials
  • govtech/cicd/github-token
This naming convention enables environment-based IAM policies that restrict access (e.g., developers can read govtech/dev/* but not govtech/prod/*).

Creating Secrets

# Create database credentials secret
aws secretsmanager create-secret \
  --name govtech/prod/db-credentials \
  --description "PostgreSQL credentials for production" \
  --kms-key-id alias/govtech-prod \
  --secret-string '{
    "username": "govtech_admin",
    "password": "SECURE_RANDOM_PASSWORD",
    "host": "govtech-prod-db.us-east-1.rds.amazonaws.com",
    "port": "5432",
    "dbname": "govtech"
  }'

# Create JWT secret
aws secretsmanager create-secret \
  --name govtech/prod/jwt-secret \
  --description "JWT signing key" \
  --kms-key-id alias/govtech-prod \
  --secret-string "$(openssl rand -base64 32)"

Updating Secrets

# Update secret value
aws secretsmanager put-secret-value \
  --secret-id govtech/prod/db-credentials \
  --secret-string '{
    "username": "govtech_admin",
    "password": "NEW_PASSWORD",
    "host": "govtech-prod-db.us-east-1.rds.amazonaws.com",
    "port": "5432",
    "dbname": "govtech"
  }'

# Verify update
aws secretsmanager describe-secret \
  --secret-id govtech/prod/db-credentials
Rolling Deployment Required:After updating a secret, restart pods to fetch the new value:
kubectl rollout restart deployment backend -n govtech
Or implement automatic secret rotation using AWS Lambda.

Retrieving Secrets

# Get secret value
aws secretsmanager get-secret-value \
  --secret-id govtech/prod/db-credentials \
  --query SecretString \
  --output text | jq .

# Output:
{
  "username": "govtech_admin",
  "password": "SECURE_PASSWORD",
  "host": "govtech-prod-db.us-east-1.rds.amazonaws.com",
  "port": "5432",
  "dbname": "govtech"
}

IRSA (IAM Roles for Service Accounts)

IRSA allows Kubernetes pods to assume IAM roles without hardcoded AWS credentials.

How IRSA Works

1

ServiceAccount with IAM Role Annotation

Create a Kubernetes ServiceAccount with an IAM role ARN:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: govtech-backend
  namespace: govtech
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::835960996869:role/govtech-backend-role
automountServiceAccountToken: true
2

IAM Role with Trust Policy

Create an IAM role that trusts the EKS cluster’s OIDC provider:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::835960996869:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:govtech:govtech-backend"
        }
      }
    }
  ]
}
3

Attach Secrets Manager Policy

Attach a policy allowing access to secrets:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:835960996869:secret:govtech/prod/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:835960996869:key/12345678-1234-1234-1234-123456789012"
    }
  ]
}
4

Deploy Pod with ServiceAccount

Reference the ServiceAccount in the deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: govtech
spec:
  template:
    spec:
      serviceAccountName: govtech-backend  # Use IRSA-enabled SA
      containers:
        - name: backend
          image: 835960996869.dkr.ecr.us-east-1.amazonaws.com/govtech-backend:latest
          env:
            - name: AWS_REGION
              value: us-east-1

Backend Code with IRSA

const AWS = require('aws-sdk');

// No credentials needed - IRSA automatically provides them
const secretsManager = new AWS.SecretsManager({
  region: process.env.AWS_REGION || 'us-east-1'
});

async function getDbCredentials() {
  try {
    const data = await secretsManager
      .getSecretValue({ SecretId: 'govtech/prod/db-credentials' })
      .promise();
    
    return JSON.parse(data.SecretString);
  } catch (error) {
    console.error('Error retrieving secret:', error);
    throw error;
  }
}

// Usage
const dbCreds = await getDbCredentials();
const connectionString = `postgresql://${dbCreds.username}:${dbCreds.password}@${dbCreds.host}:${dbCreds.port}/${dbCreds.dbname}`;

Kubernetes Secrets (Fallback)

For non-production environments or secrets that don’t require AWS Secrets Manager:

Creating Kubernetes Secrets

secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: govtech-secrets
  namespace: govtech
  labels:
    app: govtech
    component: secrets
type: Opaque
stringData:
  DATABASE_URL: "postgresql://user:[email protected]:5432/govtech_db"
  JWT_SECRET: "your-256-bit-secret"
  SESSION_SECRET: "your-session-secret"
Security Considerations:
  1. Kubernetes secrets are base64 encoded, NOT encrypted by default
  2. Enable encryption at rest for etcd:
    apiVersion: apiserver.config.k8s.io/v1
    kind: EncryptionConfiguration
    resources:
      - resources:
          - secrets
        providers:
          - aescbc:
              keys:
                - name: key1
                  secret: <base64-encoded-32-byte-key>
    
  3. Use AWS Secrets Manager for production secrets
  4. Never commit secrets.yaml to Git (use secrets.yaml.template)

Using Secrets in Pods

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: backend
          image: backend:latest
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: govtech-secrets
                  key: DATABASE_URL
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: govtech-secrets
                  key: JWT_SECRET

Secret Rotation

Automatic Rotation with Lambda

AWS Secrets Manager supports automatic rotation using Lambda functions:
# Enable rotation every 30 days
aws secretsmanager rotate-secret \
  --secret-id govtech/prod/db-credentials \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:835960996869:function:SecretsManagerRDSPostgreSQLRotationSingleUser \
  --rotation-rules AutomaticallyAfterDays=30

Manual Rotation Process

1

Create New Password

NEW_PASSWORD=$(openssl rand -base64 32)
echo $NEW_PASSWORD  # Save this temporarily
2

Update Database

ALTER USER govtech_admin WITH PASSWORD 'NEW_PASSWORD';
3

Update Secret in AWS Secrets Manager

aws secretsmanager put-secret-value \
  --secret-id govtech/prod/db-credentials \
  --secret-string '{
    "username": "govtech_admin",
    "password": "NEW_PASSWORD",
    "host": "govtech-prod-db.us-east-1.rds.amazonaws.com",
    "port": "5432",
    "dbname": "govtech"
  }'
4

Restart Application Pods

kubectl rollout restart deployment backend -n govtech
kubectl rollout status deployment backend -n govtech
5

Verify Connectivity

kubectl logs -f deployment/backend -n govtech | grep "Database connected"

Access Control

Environment-Based IAM Policies

Restrict access to production secrets:
GovTech-Secrets-Read Policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SecretsReadDev",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:*:secret:govtech/dev/*"
    },
    {
      "Sid": "SecretsReadStaging",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:us-east-1:*:secret:govtech/staging/*"
    },
    {
      "Sid": "DenyProdSecrets",
      "Effect": "Deny",
      "Action": ["secretsmanager:*"],
      "Resource": "arn:aws:secretsmanager:us-east-1:*:secret:govtech/prod/*"
    }
  ]
}
Developers can access dev/staging secrets but production is explicitly denied.

Kubernetes RBAC for Secrets

rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: govtech-backend-role
  namespace: govtech
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
    resourceNames: ["govtech-secrets"]  # Only specific secret

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: govtech-backend-binding
  namespace: govtech
subjects:
  - kind: ServiceAccount
    name: govtech-backend
    namespace: govtech
roleRef:
  kind: Role
  name: govtech-backend-role
  apiGroup: rbac.authorization.k8s.io

Audit and Monitoring

CloudTrail Logs

All secret access is logged in CloudTrail:
# View recent secret access
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceType,AttributeValue=AWS::SecretsManager::Secret \
  --max-results 20

# Check who accessed a specific secret
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=govtech/prod/db-credentials

CloudWatch Alarms

Create alerts for suspicious secret access:
# Alert on unauthorized secret access attempts
aws cloudwatch put-metric-alarm \
  --alarm-name govtech-unauthorized-secret-access \
  --alarm-description "Alert on failed secret access attempts" \
  --metric-name UnauthorizedSecretAccess \
  --namespace CloudTrailMetrics \
  --statistic Sum \
  --period 300 \
  --threshold 3 \
  --comparison-operator GreaterThanThreshold

Best Practices

Use .gitignore:
# Kubernetes secrets
kubernetes/secrets.yaml

# Environment files
.env
.env.local
.env.production

# AWS credentials
.aws/credentials
Use templates instead:
cp kubernetes/secrets.yaml.template kubernetes/secrets.yaml
# Edit secrets.yaml with real values (ignored by Git)
Rotation Schedule:
  • Database passwords: Every 90 days
  • API keys: Every 90 days
  • JWT secrets: Every 180 days (requires coordinated deployment)
  • Access keys: Every 90 days (or use temporary credentials)
Automate rotation:
aws secretsmanager rotate-secret \
  --secret-id govtech/prod/db-credentials \
  --rotation-rules AutomaticallyAfterDays=90
Always encrypt secrets with KMS:
aws secretsmanager create-secret \
  --name govtech/prod/api-key \
  --kms-key-id alias/govtech-prod \
  --secret-string "secret-value"
Benefits:
  • Centralized key management
  • Audit trail for key usage
  • Automatic key rotation
  • Fine-grained access control
Principle of least privilege for secrets:
  • Create separate secrets for each service
  • Limit secret permissions to specific IAM roles
  • Use resource tags for access control
Example:
{
  "Effect": "Allow",
  "Action": "secretsmanager:GetSecretValue",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "secretsmanager:ResourceTag/Service": "backend"
    }
  }
}

Troubleshooting

Error: AccessDenied: User is not authorized to perform secretsmanager:GetSecretValueDiagnosis:
# Check if IRSA is configured
kubectl describe sa govtech-backend -n govtech | grep Annotations

# Verify IAM role exists
aws iam get-role --role-name govtech-backend-role

# Test from pod
kubectl exec -it <backend-pod> -n govtech -- env | grep AWS
Solution:
  1. Verify ServiceAccount has IAM role annotation
  2. Check IAM role trust policy allows OIDC provider
  3. Ensure IAM policy allows secretsmanager:GetSecretValue
Error: AccessDeniedException: User is not authorized to perform kms:DecryptCause: Secret is encrypted with KMS, but IAM role lacks decrypt permission.Solution: Add KMS permissions to IAM role:
{
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:DescribeKey"
  ],
  "Resource": "arn:aws:kms:us-east-1:835960996869:key/12345678-1234-1234-1234-123456789012"
}

Next Steps

IAM Policies

Review IAM policies for secrets access

Compliance

Audit procedures and compliance frameworks

Build docs developers (and LLMs) love