14 KiB
14 KiB
DevSecOps & Security Best Practices
Security Principles
Defense in Depth
- Multiple layers of security controls
- Network security, application security, data security
- No single point of failure
Least Privilege
- Minimum permissions necessary
- Regular access reviews
- Time-bound elevated access
Zero Trust
- Never trust, always verify
- Verify every request regardless of origin
- Micro-segmentation and strict access controls
Secrets Management
❌ Never Do This
# NEVER hardcode secrets!
apiVersion: v1
kind: Pod
metadata:
name: bad-example
spec:
containers:
- name: app
env:
- name: DATABASE_PASSWORD
value: "SuperSecret123!" # 🚨 NEVER DO THIS
- name: API_KEY
value: "sk-abc123def456" # 🚨 NEVER DO THIS
✅ Use External Secrets Operator
# External Secrets with AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
data:
- secretKey: database-password
remoteRef:
key: prod/myapp/database-password
- secretKey: api-key
remoteRef:
key: prod/myapp/api-key
✅ Use Sealed Secrets
# Install kubeseal
brew install kubeseal
# Create sealed secret
kubectl create secret generic myapp-secrets \
--from-literal=db-password='secret123' \
--dry-run=client -o yaml | \
kubeseal -o yaml > sealed-secret.yaml
# Apply sealed secret (safe to commit to Git)
kubectl apply -f sealed-secret.yaml
AWS Secrets Manager with IRSA
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/myapp-secrets-role
---
# Application retrieves secrets using AWS SDK
# No credentials in code or config!
Container Security
Secure Dockerfile
# Use specific version tags, not 'latest'
FROM node:20.10.0-alpine3.19 AS builder
# Run as non-root user
RUN addgroup -g 1000 nodejs && \
adduser -u 1000 -G nodejs -s /bin/sh -D nodejs
WORKDIR /app
# Copy and install dependencies
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# Copy application code
COPY --chown=nodejs:nodejs . .
# Build application
RUN npm run build
# Production stage
FROM node:20.10.0-alpine3.19
# Install dumb-init for signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1000 nodejs && \
adduser -u 1000 -G nodejs -s /bin/sh -D nodejs
WORKDIR /app
# Copy from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
# Switch to non-root user
USER nodejs
# Use dumb-init
ENTRYPOINT ["dumb-init", "--"]
# Run application
CMD ["node", "dist/index.js"]
# Expose port
EXPOSE 8080
# Add labels
LABEL org.opencontainers.image.source="https://github.com/myorg/myapp" \
org.opencontainers.image.version="1.0.0" \
org.opencontainers.image.vendor="MyOrg"
Security Scanning
Trivy (Container Vulnerability Scanner)
# Scan Docker image
trivy image myapp:latest
# Scan with severity filter
trivy image --severity HIGH,CRITICAL myapp:latest
# Scan filesystem
trivy fs --security-checks vuln,config /path/to/project
# Output to JSON
trivy image -f json -o results.json myapp:latest
Grype (Vulnerability Scanner)
# Scan image
grype myapp:latest
# Scan with specific severity
grype myapp:latest --fail-on high
# Output SARIF for GitHub
grype myapp:latest -o sarif > results.sarif
Pod Security Standards
# Pod Security Admission
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
# Secure Pod Configuration
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if needed
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Network Security
Network Policies
# Default deny all ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Allow specific ingress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-allow-ingress
namespace: production
spec:
podSelector:
matchLabels:
app: myapp
policyTypes:
- Ingress
- Egress
ingress:
# Allow from ingress controller
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
name: kube-system
- podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
# Allow database
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Allow HTTPS egress
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443
AWS Security Groups (Terraform)
# Application Security Group
resource "aws_security_group" "app" {
name = "${var.name_prefix}-app-sg"
description = "Security group for application"
vpc_id = var.vpc_id
# Allow inbound from ALB only
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "HTTP from ALB"
}
# Allow outbound to database
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.database.id]
description = "PostgreSQL to database"
}
# Allow HTTPS egress
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS to internet"
}
tags = var.tags
}
IAM & RBAC
Kubernetes RBAC
# Least privilege service account
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: production
---
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: myapp-role
namespace: production
rules:
# Only read ConfigMaps and Secrets
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
resourceNames: ["myapp-config", "myapp-secrets"]
# Read own pod information
- apiGroups: [""]
resources: ["pods"]
verbs: ["get"]
---
# Bind role to service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-rolebinding
namespace: production
subjects:
- kind: ServiceAccount
name: myapp
namespace: production
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io
AWS IAM Policy (Terraform)
# Least privilege IAM policy
data "aws_iam_policy_document" "app" {
# Allow reading specific secrets
statement {
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
]
resources = [
"arn:aws:secretsmanager:${var.region}:${data.aws_caller_identity.current.account_id}:secret:prod/myapp/*"
]
}
# Allow writing to specific S3 bucket
statement {
actions = [
"s3:PutObject",
"s3:GetObject",
]
resources = [
"${aws_s3_bucket.app.arn}/*"
]
}
# Allow publishing to specific SNS topic
statement {
actions = [
"sns:Publish",
]
resources = [
aws_sns_topic.app_notifications.arn
]
}
}
resource "aws_iam_policy" "app" {
name = "${var.name_prefix}-app-policy"
policy = data.aws_iam_policy_document.app.json
}
Compliance & Policy as Code
OPA (Open Policy Agent)
# Deny pods running as root
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
input.request.object.spec.securityContext.runAsNonRoot != true
msg := "Pods must not run as root"
}
# Require resource limits
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits
msg := sprintf("Container %v must specify resource limits", [container.name])
}
# Deny latest tag
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container %v uses 'latest' tag", [container.name])
}
# Require specific labels
deny[msg] {
input.request.kind.kind == "Pod"
required_labels := ["app", "version", "environment"]
label := required_labels[_]
not input.request.object.metadata.labels[label]
msg := sprintf("Missing required label: %v", [label])
}
Terraform Sentinel Policy
# sentinel.hcl
policy "require-tags" {
enforcement_level = "hard-mandatory"
}
policy "restrict-instance-type" {
enforcement_level = "soft-mandatory"
}
# require-tags.sentinel
import "tfplan/v2" as tfplan
required_tags = ["Environment", "Owner", "CostCenter"]
main = rule {
all tfplan.resource_changes as _, rc {
rc.mode is "managed" and
rc.type in ["aws_instance", "aws_db_instance", "aws_s3_bucket"] and
rc.change.actions contains "create"
implies
all required_tags as tag {
rc.change.after.tags contains tag
}
}
}
Security Scanning in CI/CD
GitHub Actions Security Workflow
name: Security Scan
on: [push, pull_request]
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
terraform-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
framework: terraform
soft_fail: false
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: auto
Encryption
At Rest
# S3 bucket encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true
}
}
# RDS encryption
resource "aws_db_instance" "app" {
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
# ... other configuration
}
# EBS encryption
resource "aws_ebs_volume" "app" {
encrypted = true
kms_key_id = aws_kms_key.ebs.arn
# ... other configuration
}
In Transit
# Enforce TLS in Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
tls:
- hosts:
- app.example.com
secretName: app-tls
Security Best Practices Checklist
- Never hardcode secrets - Use secret managers
- Run containers as non-root - Set securityContext
- Use specific image tags - Never use 'latest'
- Scan images for vulnerabilities - Use Trivy/Grype
- Enable Pod Security Standards - Use restricted profile
- Implement network policies - Default deny, allow specific
- Use RBAC - Least privilege access
- Encrypt data at rest - KMS for all data stores
- Enforce TLS - All traffic encrypted in transit
- Scan IaC for issues - Checkov, tfsec in CI/CD
- Rotate credentials regularly - Automate rotation
- Audit and log everything - CloudTrail, audit logs
- Implement policy as code - OPA, Sentinel
- Regular security reviews - Penetration testing, audits
- Keep dependencies updated - Renovate, Dependabot
Security Incident Response
- Detect: Automated alerting on security events
- Contain: Isolate affected systems
- Eradicate: Remove threat and vulnerabilities
- Recover: Restore services securely
- Learn: Post-incident review and improvements