chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
# 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
|
||||
```yaml
|
||||
# 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
|
||||
```yaml
|
||||
# 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
|
||||
```bash
|
||||
# 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
|
||||
```yaml
|
||||
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
|
||||
```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)
|
||||
```bash
|
||||
# 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)
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```yaml
|
||||
# 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
|
||||
```yaml
|
||||
# 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)
|
||||
```hcl
|
||||
# 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
|
||||
```yaml
|
||||
# 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)
|
||||
```hcl
|
||||
# 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)
|
||||
```rego
|
||||
# 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
|
||||
```hcl
|
||||
# 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
|
||||
```yaml
|
||||
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
|
||||
```hcl
|
||||
# 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
|
||||
```yaml
|
||||
# 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
|
||||
|
||||
1. **Detect**: Automated alerting on security events
|
||||
2. **Contain**: Isolate affected systems
|
||||
3. **Eradicate**: Remove threat and vulnerabilities
|
||||
4. **Recover**: Restore services securely
|
||||
5. **Learn**: Post-incident review and improvements
|
||||
Reference in New Issue
Block a user