Long-lived credentials — static API keys, hardcoded database passwords, static IAM access keys — are the most common finding in government cloud security assessments. They violate NIST 800-53 IA-5(7) (No Embedded Unencrypted Static Authenticators) and create persistent lateral movement risk if any credential is compromised. Rutagon's zero long-lived credential posture isn't aspirational — it's operational in our production infrastructure.
Here's how to eliminate static credentials from a GovCloud environment using native AWS services.
The Problem: Why Static Credentials Persist
Static credentials persist because they're easy. Copy an IAM access key into a CI/CD environment variable and you're done. The problem: that key lives forever (unless manually rotated), has no context about where it's being used, can be exfiltrated from memory or logs, and is often copy-pasted into places you didn't intend.
The NIST 800-53 control family that addresses this:
- IA-5(7): Ensure systems don't embed unencrypted static authenticators
- IA-5(1): Password management — enforced complexity and rotation
- IA-2(1)(2): MFA for all privileged access
- SC-12: Cryptographic key establishment — managed lifecycle, not manual
The fix is architectural: replace static credentials with short-lived, contextually-bound credentials issued dynamically at runtime.
Three Pillars of GovCloud Secrets Management
1. IAM Roles for Service Accounts (IRSA) — Eliminate EC2 and EKS Instance Credentials
For any workload running on EKS, IRSA provides IAM permissions without any credentials. The pod's Kubernetes service account is federated to an IAM role via OIDC — AWS issues a short-lived token valid for 1 hour.
# Terraform: IRSA for a data pipeline pod
resource "aws_iam_role" "data_pipeline_pod_role" {
name = "data-pipeline-pod-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_oidc_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${replace(aws_iam_oidc_provider.eks.url, "https://", "")}:sub" : "system:serviceaccount:production:data-pipeline-sa"
"${replace(aws_iam_oidc_provider.eks.url, "https://", "")}:aud" : "sts.amazonaws.com"
}
}
}]
})
}
resource "aws_iam_role_policy" "data_pipeline_s3_access" {
name = "s3-read-access"
role = aws_iam_role.data_pipeline_pod_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.data_bucket.arn,
"${aws_s3_bucket.data_bucket.arn}/*"
]
}]
})
} The pod's Kubernetes manifest annotates the service account with the role ARN:
apiVersion: v1
kind: ServiceAccount
metadata:
name: data-pipeline-sa
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws-us-gov:iam::123456789:role/data-pipeline-pod-role No access key. No secret key. The pod gets a fresh token on startup, AWS SDK refreshes it automatically, and there's nothing to rotate, protect, or audit separately.
2. AWS Secrets Manager — Application Credentials
For credentials that can't be eliminated (third-party API keys, database passwords, SMTP credentials), AWS Secrets Manager provides centralized storage with automatic rotation.
resource "aws_secretsmanager_secret" "db_credentials" {
name = "/production/database/credentials"
kms_key_id = aws_kms_key.secrets_cmk.id
recovery_window_in_days = 7
tags = {
Classification = "CUI"
Environment = "production"
}
}
resource "aws_secretsmanager_secret_rotation" "db_auto_rotation" {
secret_id = aws_secretsmanager_secret.db_credentials.id
rotation_lambda_arn = aws_lambda_function.secret_rotator.arn
rotation_rules {
automatically_after_days = 30
}
} Critical for GovCloud: Use a customer-managed KMS key (CMK) for Secrets Manager encryption — not the default AWS-managed key. CMKs satisfy SC-12 and SC-28 requirements for cryptographic key management, and provide explicit key policy control over who can decrypt secrets.
Application retrieval at runtime (Python example):
import boto3
import json
from functools import lru_cache
@lru_cache(maxsize=None)
def get_secret(secret_name: str) -> dict:
client = boto3.client('secretsmanager', region_name='us-gov-west-1')
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
# Usage — no credentials in code or environment
db_creds = get_secret('/production/database/credentials')
connection = psycopg2.connect(
host=db_creds['host'],
database=db_creds['dbname'],
user=db_creds['username'],
password=db_creds['password']
) The application retrieves credentials at startup — if rotation happens mid-day, the next cache miss fetches the new credentials. No deployment required for credential rotation.
3. SSM Parameter Store — Configuration and Non-Sensitive Secrets
Parameter Store handles configuration values that don't require Secrets Manager's rotation features — environment-specific endpoints, feature flags, and low-sensitivity configuration.
resource "aws_ssm_parameter" "api_endpoint" {
name = "/production/external-api/endpoint"
type = "SecureString"
value = "https://api.mission-system.gov/v2"
tier = "Standard"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
} Cost consideration: SSM Parameter Store Standard tier is free; Secrets Manager costs approximately $0.40/secret/month. For GovCloud environments with hundreds of config values, use Parameter Store for configuration and Secrets Manager for credentials requiring rotation.
CI/CD: OIDC Federation for GitLab to GovCloud
CI/CD pipelines are a common static credential vector. GitLab Runner deploying to GovCloud with an IAM access key in a CI variable is a standing security debt. OIDC federation eliminates this:
# Trust policy allowing GitLab to assume this role
data "aws_iam_policy_document" "gitlab_cicd_trust" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.gitlab.arn]
}
actions = ["sts:AssumeRoleWithWebIdentity"]
condition {
test = "StringLike"
variable = "gitlab.example.gov:sub"
values = ["project_path:your-org/your-repo:ref_type:branch:ref:main"]
}
}
} # .gitlab-ci.yml — OIDC authentication, no stored credentials
assume-role:
script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn arn:aws-us-gov:iam::123456789:role/GitLabDeployRole
--role-session-name gitlab-ci-${CI_JOB_ID}
--web-identity-token $CI_JOB_JWT_V2
--duration-seconds 3600
--query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]"
--output text))
- terraform apply -auto-approve This pattern is what Rutagon runs in production across our internal multi-account AWS Organization. No long-lived credentials anywhere in the pipeline. Every deployment uses a fresh 1-hour token scoped to exactly what the deployment needs.
Terraform: Preventing Credentials in State
Terraform state files store resource data — including, sometimes, sensitive values. If your state is stored in S3, ensure:
- Server-side encryption with CMK: Every state bucket must have SSE-KMS with a CMK
- State locking with DynamoDB: Prevents concurrent writes that corrupt state
- Sensitive value handling: Mark sensitive outputs and variables; reference Terraform state management in GovCloud for the full backend configuration
# NEVER hardcode sensitive values in Terraform
# BAD:
variable "db_password" {
default = "supersecret123" # This ends up in state and plan output
}
# GOOD: Reference from Secrets Manager at deployment time
data "aws_secretsmanager_secret_version" "db_creds" {
secret_id = "/production/database/credentials"
}
locals {
db_password = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)["password"]
} Audit Trail: Secrets Access for IA-5 Evidence
Every Secrets Manager API call generates a CloudTrail event. For ConMon purposes, configure a CloudWatch Metric Filter to alert on unusual access patterns:
resource "aws_cloudwatch_metric_alarm" "secrets_access_anomaly" {
alarm_name = "secrets-manager-unusual-access"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "5"
metric_name = "SecretsManagerAccessCount"
namespace = "Custom/SecretsManager"
period = "60"
statistic = "Sum"
threshold = "50"
alarm_description = "High volume Secrets Manager access — possible credential harvesting"
alarm_actions = [aws_sns_topic.security_alerts.arn]
} This alarm satisfies SI-4 (System Monitoring) and supports the AU-6 (Audit Review) requirement for anomaly detection.
Related Resources
This architecture pairs with AWS GovCloud Infrastructure as Code patterns, DISA STIG compliance automation for cloud deployments, and Rutagon's CI/CD pipeline for government programs.
Rutagon operates this secrets management architecture in production — zero long-lived credentials, OIDC-federated CI/CD, and IRSA-based pod identity across our entire AWS Organization. Contact Rutagon to implement this pattern on your federal program.
Frequently Asked Questions
What is IRSA and why should government EKS workloads use it?
IRSA (IAM Roles for Service Accounts) federates Kubernetes pod identity to IAM roles via OIDC, issuing short-lived tokens (1 hour) automatically. Government EKS workloads should use IRSA because it eliminates the static IAM access keys that cause IA-5(7) findings, scopes permissions to the specific namespace and service account (AC-6 least privilege), and leaves an accurate CloudTrail audit trail showing exactly which pod assumed which role.
Is AWS Secrets Manager available in GovCloud?
Yes. AWS Secrets Manager is available in both us-gov-east-1 and us-gov-west-1 GovCloud regions. It supports automatic rotation via Lambda, CMK encryption, fine-grained IAM policies, and VPC endpoints so that secrets retrieval doesn't leave the GovCloud network boundary. All Secrets Manager API calls are logged to CloudTrail.
How does this approach satisfy NIST 800-53 IA-5?
IA-5(1) requires password complexity and rotation — Secrets Manager's automatic rotation satisfies this for database credentials. IA-5(7) prohibits embedded unencrypted static authenticators — IRSA and OIDC federation eliminate static credentials entirely. IA-5(2) requires PKI certificate validation — IRSA uses X.509 certificates through the OIDC mechanism. Together, these patterns provide documentary evidence for the full IA-5 control family.
What's the difference between Secrets Manager and Parameter Store?
AWS Secrets Manager is purpose-built for credentials — it includes automatic rotation via Lambda, cross-account access, and costs ~$0.40/secret/month. SSM Parameter Store SecureString provides encrypted storage with KMS but no built-in rotation. Use Secrets Manager for any credential that should be rotated (database passwords, API keys). Use Parameter Store for configuration values — endpoints, feature flags, non-sensitive settings. Both use KMS encryption and generate CloudTrail events.
How do we handle secret rotation without application downtime?
Secrets Manager's rotation Lambda follows the "create new, test, delete old" pattern. For zero-downtime rotation: the application should retrieve secrets from Secrets Manager at connection time (not startup), implement retry logic with fresh secret retrieval on authentication failure, and use connection pooling that can acquire new credentials mid-run. This pattern keeps the application running while the underlying credential rotates without a deployment.
Discuss your project with Rutagon
Contact Us →