DevSecOps
IaC
Terraform
Docker
Kubernetes
+3 more

IaC Security: Securing Terraform, Docker & Kubernetes Before Deployment

SCR Security Research Team
February 1, 2026
21 min read
Share

Infrastructure as Code Is Infrastructure as Vulnerability

Infrastructure as Code (IaC) transformed how we deploy systems — but it also codified our misconfigurations. According to Bridgecrew's 2025 State of IaC Security Report:

FindingValue
IaC templates with at least one misconfiguration67%
Average misconfigurations per template12.7
Most common: overly permissive IAM policies38% of all misconfigs
Second: unencrypted data storage29%
Third: excessive network exposure22%

The Advantage: Unlike manual infrastructure, IaC misconfigurations can be found and fixed before deployment. This is the power of shift-left for infrastructure — scan the code, not the running system.


Terraform Security

The 10 Most Common Terraform Misconfigurations

#MisconfigurationRiskFix
1S3 bucket public accessData exposureblock_public_access = true
2Security group 0.0.0.0/0 on SSHUnauthorized accessRestrict to known CIDRs
3No encryption at restData exposureEnable encryption on all storage
4IAM * permissionsPrivilege escalationLeast-privilege policies
5No CloudTrail loggingNo audit trailEnable multi-region CloudTrail
6Default VPC usageShared network riskCreate custom VPC
7No MFA delete on S3Accidental/malicious deletionEnable MFA delete
8RDS publicly accessibleDatabase exposurepublicly_accessible = false
9No lifecycle policiesCost + stale resourcesSet TTLs and rotation
10Hardcoded secretsCredential exposureUse aws_secretsmanager_secret

Secure Terraform Patterns

# SECURE — S3 bucket with full security hardening
resource "aws_s3_bucket" "data" {
  bucket = "company-data-prod"
}

resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
  }
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_logging" "data" {
  bucket        = aws_s3_bucket.data.id
  target_bucket = aws_s3_bucket.logs.id
  target_prefix = "s3-access-logs/"
}

Terraform Security Scanning Tools

ToolChecksIntegration
Checkov2,500+ rules across AWS/Azure/GCPCLI, CI/CD, IDE
tfsecTerraform-specific security rulesCLI, GitHub Actions
TerrascanMulti-IaC (Terraform, K8s, Helm)CLI, CI/CD
SentinelHashiCorp policy-as-codeTerraform Cloud/Enterprise
OPA/ConftestGeneral purpose policy engineAny IaC format

Docker Security

Docker Security Hardening Checklist

# SECURE — Production-ready Dockerfile

# 1. Use specific, minimal base image (not 'latest')
FROM node:20-alpine AS builder

# 2. Create non-root user
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# 3. Set working directory
WORKDIR /app

# 4. Copy package files first (layer caching)
COPY package.json package-lock.json ./

# 5. Install dependencies (production only, ignore scripts)
RUN npm ci --only=production --ignore-scripts

# 6. Copy application code
COPY --chown=appuser:appgroup . .

# 7. Build application
RUN npm run build

# 8. Multi-stage build — production image
FROM node:20-alpine AS production

# 9. Install security updates
RUN apk update && apk upgrade --no-cache

# 10. Create non-root user in production image
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

WORKDIR /app

# 11. Copy only production artifacts
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules

# 12. Drop all capabilities, run as non-root
USER appuser

# 13. Set read-only filesystem where possible
# (configure at runtime with --read-only flag)

# 14. Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

# 15. Expose only required port
EXPOSE 3000

CMD ["node", "dist/server.js"]

Container Scanning

# Scan Docker image for vulnerabilities
trivy image myapp:latest --severity HIGH,CRITICAL

# Scan and fail on critical findings (CI/CD integration)
trivy image myapp:latest --severity CRITICAL --exit-code 1

# Scan Dockerfile for misconfigurations
trivy config ./Dockerfile

Kubernetes Security

K8s Security Fundamentals

AreaRiskControl
RBACOver-privileged service accountsLeast-privilege ClusterRoles
Pod SecurityContainers running as rootPod Security Standards (restricted)
NetworkUnrestricted pod-to-pod trafficNetwork Policies
SecretsSecrets in etcd unencryptedExternal secrets management
ImagesPulling untrusted imagesImage allowlists, signing
RuntimeContainer escapeSeccomp, AppArmor, read-only FS

Secure Pod Configuration

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  # Don't use default service account
  serviceAccountName: app-specific-sa
  automountServiceAccountToken: false

  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    fsGroup: 1001
    seccompProfile:
      type: RuntimeDefault

  containers:
    - name: app
      image: myregistry.com/app:v1.2.3@sha256:abc123...
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      resources:
        limits:
          cpu: "500m"
          memory: "256Mi"
        requests:
          cpu: "100m"
          memory: "128Mi"
      livenessProbe:
        httpGet:
          path: /health
          port: 3000
        initialDelaySeconds: 10
      volumeMounts:
        - name: tmp
          mountPath: /tmp

  volumes:
    - name: tmp
      emptyDir: {}

Kubernetes Network Policy (Zero-Trust Networking)

# Default deny all traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

---
# Allow only specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - port: 3000
          protocol: TCP

IaC Security in CI/CD

Automated IaC Security Pipeline

# Combined IaC security scanning
name: IaC Security
on: [pull_request]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Checkov Terraform scan
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./terraform
          framework: terraform
          soft_fail: false  # Block PR on failures

  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Hadolint Dockerfile lint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
      - name: Build and scan
        run: |
          docker build -t test:latest .
          trivy image test:latest --exit-code 1 --severity HIGH,CRITICAL

  kubernetes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: KubeSec scan
        run: |
          kubesec scan k8s/*.yaml
      - name: Checkov K8s scan
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./k8s
          framework: kubernetes

Further Reading

Advertisement