๐ก๏ธ DevSecOps, Supply Chain & Fleet Patching Triage Guide
This guide details the most common, tricky, and critical production failure modes encountered across container security scanning (Trivy), cryptographic signing (Cosign / Sigstore), Kubernetes secret synchronization (External Secrets Operator), Golden AMI baking (Packer), and fleet patching (ASG Instance Refresh).
๐งญ Beginner-Friendly First-5-Minutes DevSecOps & Supply Chain Playbook (Step-by-Step SOP)
Mental Model (The Airport Passport Control): Supply chain security works like airport border control: 1. CI OIDC mints a short-lived passport โ 2. Cosign stamps the container image digest โ 3. Kyverno checks the stamp at the Kubernetes gate โ 4. External Secrets Operator (ESO) delivers the room key.
Step 1: Check Admission Controller Rejection Reason (Minute 0โ1)
kubectl get events -n <namespace> --field-selector reason=PolicyViolation
kubectl logs -n kyverno -l app.kubernetes.io/component=admission-controller --tail=50
Step 2: Verify Container Image Digest & Cosign Signature (Minute 1โ2)
cosign verify <registry>/<image>@sha256:<digest> \
--certificate-identity-regexp="https://github.com/org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"
Step 3: Inspect External Secrets Operator (ESO) Synchronization Status (Minute 2โ3)
kubectl get secretstores,externalsecrets -A
kubectl describe externalsecret <name> -n <namespace>
Step 4: Check ASG Instance Refresh & PDB Drain Blockers (Minute 3โ5)
aws autoscaling describe-instance-refreshes --auto-scaling-group-name <asg-name>
kubectl get pdb -A
๐ฏ High-Impact Real-World Scenarios
Scenario 1: Air-Gapped EKS Cosign Keyless Verification Failure
The Symptom
Pods scheduled in a PCI-DSS compliant or air-gapped private EKS cluster fail admission with:
Error from server (InternalError): admission webhook "validate.kyverno.io" denied the request:
failed to verify image signature: failed to fetch rekor public keys:
dial tcp 34.120.12.55:443: connect: connection timed out
Under-the-Hood Root Cause
The CI/CD pipeline used standard keyless Sigstore signing (cosign sign --yes), which publishes cryptographic transparency entries to the public Sigstore Rekor ledger (rekor.sigstore.dev).
However, the production Kubernetes cluster resides inside an isolated, air-gapped private VPC with zero public internet access. When Kyverno or the admission controller attempts to verify the signature against Rekor, the outbound TCP connection times out, blocking all pod creation!
sequenceDiagram
autonumber
participant K8s as EKS API Server
participant Kyverno as Kyverno Admission Webhook
participant Rekor as ๐ Public Rekor (rekor.sigstore.dev)
participant Pod as Microservice Pod
K8s->>Kyverno: Admission Request (New Pod)
Kyverno->>Rekor: โ Verifies against public Rekor over Internet
Note over Kyverno,Rekor: Private Subnet: No Internet Gateway or NAT Gateway!<br/>Connection times out after 15s! ๐จ
Kyverno-->>K8s: Admission Denied: Rekor Connection Timed Out!
Note over K8s,Pod: Pod fails to schedule! Production halted!
Remediation & Architectural Standard
- The Solution (Private AWS KMS Asymmetric Signing): For air-gapped enterprise environments, do NOT rely on public Sigstore infrastructure. Use AWS KMS Asymmetric Keys (ECC NIST P-256 or RSA-4096) with AWS PrivateLink endpoints:
# 1. Sign image via private AWS KMS VPC Endpoint
cosign sign --yes \
--key awskms://arn:aws:kms:us-east-1:123456789012:key/b6d1911e-450a-4a52-b810-platform-signing \
123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-service:v2.1.0
# 2. Configure Kyverno to verify using KMS key ARN over AWS VPC Endpoint
Scenario 2: External Secrets Operator (ESO) AWS API Throttling Storm
The Symptom
External Secrets Operator fails to synchronize secrets. The controller logs report:
ThrottlingException: Rate exceeded:
AWS Secrets Manager API request limit exceeded (TPS limit reached).
Kubernetes pods requiring fresh credentials fail to launch or read outdated values.
Under-the-Hood Root Cause
Developers configured 500 ExternalSecret manifests across 30 namespaces with an aggressive refreshInterval: 10s. AWS Secrets Manager enforces a regional API call limit of 5,000 TPS for GetSecretValue.
With 500 secrets being queried every 10 seconds, background polling consumed a massive percentage of the accountโs quota. When an automated deployment triggered 100 new pods simultaneously, the combined API volume exceeded the quota, triggering hard rate-limiting.
CLI Triage Commands
# 1. Inspect failed ExternalSecret statuses across all namespaces
kubectl get externalsecrets -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,STORE:.spec.secretStoreRef.name,READY:.status.conditions[?(@.type=="Ready")].status,MESSAGE:.status.conditions[?(@.type=="Ready")].message | grep -i "rate"
# 2. Check ESO controller logs
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets --tail=100
Remediation & Architectural Fix
- The Standard: Change the default
refreshIntervalacross allExternalSecretmanifests from seconds to hours:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-db-credentials
spec:
refreshInterval: "12h" # Poll once every 12 hours instead of every 10s!
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: payment-db-secret
- Event-Driven Refresh: Instead of aggressive polling, trigger on-demand syncs via AWS EventBridge webhooks whenever AWS Secrets Manager executes a scheduled rotation.
Scenario 3: Golden AMI /etc/machine-id Duplication Disaster
The Symptom
When 200 EC2 worker nodes boot from a newly baked Golden AMI, half of the instances cannot connect to the network, and the corporate DHCP/IPAM server logs report:
DHCPACK: duplicate IP assignment detected for client-id: 7f8a92b1...
Multiple EC2 instances receive identical IP addresses or overwrite each otherโs registration records in internal DNS.
Under-the-Hood Root Cause
During the Packer build process, systemd generated a unique /etc/machine-id file when the temporary builder VM booted. The Packer provisioning script failed to truncate or delete /etc/machine-id before taking the EBS snapshot. As a result, all 200 instances booted with the exact same machine-id, which Linux DHCP clients use as the unique hardware client identifier!
Remediation & Sysprep Best Practice
Always include a cleanup script at the end of the Packer provisioning pipeline:
# Sysprep script: /opt/platform/scripts/sysprep-cleanup.sh
# 1. Truncate machine-id (leaves file empty so systemd re-generates on first boot)
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
# 2. Remove persistent SSH host keys
sudo rm -f /etc/ssh/ssh_host_*
# 3. Clean cloud-init caches
sudo cloud-init clean --all --logs
Scenario 4: ASG Instance Refresh Deadlock on Lifecycle Hook Timeout
The Symptom
An automated fleet patching pipeline triggers StartInstanceRefresh on an AWS Auto Scaling Group. The refresh immediately halts at 0% progress:
InstanceRefreshStatus: Failed (Failed to replace instances:
Instance i-0123456789abcdef0 failed to enter InService state before heartbeat timeout expired).
Under-the-Hood Root Cause
The ASG uses an autoscaling:EC2_INSTANCE_LAUNCHING lifecycle hook to run configuration scripts (e.g. downloading certificates, joining Kubernetes cluster). The script failed or hung waiting for an internal repository, and did NOT issue the completion signal:
aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE ...
When the 1-hour heartbeat timeout expired, AWS terminated the instance and aborted the entire fleet refresh!
CLI Triage Commands
# 1. Check active lifecycle actions on the failing instance
aws autoscaling describe-lifecycle-actions \
--auto-scaling-group-name prod-worker-asg
# 2. Inspect instance system console logs to see where user-data hung
aws ec2 get-console-output --instance-id i-0123456789abcdef0
Remediation & Architectural Standard
- Fail-Safe Shell Scripts: Always wrap user-data lifecycle scripts with error trapping and timeouts:
#!/usr/bin/env bash
set -e
cleanup_on_failure() {
# Send ABANDON so ASG fails fast rather than hanging for 1 hour
aws autoscaling complete-lifecycle-action \
--lifecycle-action-result ABANDON \
--lifecycle-hook-name node-init-hook \
--auto-scaling-group-name "$ASG_NAME" \
--instance-id "$INSTANCE_ID"
}
trap cleanup_on_failure ERR