Day 24 - Harden a Config
Apply security hardening best practices to application configurations, containers, and infrastructure.
Description
Your infrastructure configurations have security issues: default passwords, overly permissive access, missing security headers, and vulnerable settings. Apply security hardening to protect against attacks.
Task
Harden application and infrastructure configurations.
Requirements:
- Apply security best practices
- Remove default credentials
- Implement least privilege
- Add security headers
- Enable security features
Target
- ✅ No default credentials
- ✅ Principle of least privilege applied
- ✅ Security headers enabled
- ✅ Unnecessary features disabled
- ✅ Security scans pass
Sample App
Before Hardening (Insecure)
Dockerfile (Insecure)
FROM ubuntu:latest
# Running as root!
USER root
# Installing unnecessary packages
RUN apt-get update && apt-get install -y \
curl wget vim sudo openssh-server
# Default passwords
RUN echo 'root:password123' | chpasswd
# Exposing all ports
EXPOSE 1-65535
# No health check
CMD ["/bin/bash"]
nginx.conf (Insecure)
server {
listen 80;
server_name _;
# Server version exposed
server_tokens on;
# No security headers
location / {
root /usr/share/nginx/html;
autoindex on; # Directory listing enabled!
# CORS wide open
add_header Access-Control-Allow-Origin *;
}
}
View Solution
Solution
1. Hardened Dockerfile
# Use specific version, not latest
FROM node:20.10-alpine3.18
# Security labels
LABEL maintainer="[email protected]"
LABEL org.opencontainers.image.description="Hardened application container"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
# Install security updates
RUN apk update && \
apk upgrade && \
apk add --no-cache \
dumb-init \
&& rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1001 -S appuser && \
adduser -S -D -H -u 1001 -h /app -s /sbin/nologin -G appuser appuser
# Set working directory
WORKDIR /app
# Copy package files
COPY --chown=appuser:appuser package*.json ./
# Install dependencies (only production)
RUN npm ci --only=production && \
npm cache clean --force && \
rm -rf /tmp/*
# Copy application files
COPY --chown=appuser:appuser . .
# Remove unnecessary files
RUN rm -rf \
.git \
.github \
tests \
*.md \
.env.example
# Set file permissions
RUN chmod -R 550 /app && \
chmod -R 770 /app/logs
# Switch to non-root user
USER appuser
# Use dumb-init to handle signals
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# Expose only required port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
# Run with minimal privileges
CMD ["node", "--max-old-space-size=512", "server.js"]
# Security options (use with docker run)
# --read-only --tmpfs /tmp --tmpfs /app/logs
# --security-opt=no-new-privileges:true
# --cap-drop=ALL --cap-add=NET_BIND_SERVICE
2. Hardened nginx Configuration
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
# Hide version
server_tokens off;
more_clear_headers Server;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Buffer overflow protection
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 1k;
large_client_header_buffers 2 1k;
# Timeouts
client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 5 5;
send_timeout 10;
# SSL/TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
server {
listen 80;
server_name example.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL certificates
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Rate limiting
limit_req zone=general burst=20 nodelay;
limit_conn addr 10;
# Logging
access_log /var/log/nginx/access.log combined;
error_log /var/log/nginx/error.log warn;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
# Disable directory listing
autoindex off;
}
location /api {
# API rate limiting
limit_req zone=api burst=10 nodelay;
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
# Buffer sizes
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# Block access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block access to sensitive files
location ~* \.(env|log|git|svn|htaccess)$ {
deny all;
}
# Health check (no logging)
location /health {
access_log off;
return 200 "healthy\n";
}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /404.html {
internal;
}
location = /50x.html {
internal;
}
}
}
3. Kubernetes Security
# hardened-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: production
automountServiceAccountToken: false # Disable auto-mounting
---
apiVersion: v1
kind: Secret
metadata:
name: myapp-token
namespace: production
annotations:
kubernetes.io/service-account.name: myapp
type: kubernetes.io/service-account-token
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
# Pod security annotations
container.apparmor.security.beta.kubernetes.io/myapp: runtime/default
spec:
serviceAccountName: myapp
# Security context for pod
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
fsGroupChangePolicy: "OnRootMismatch"
seccompProfile:
type: RuntimeDefault
containers:
- name: myapp
image: myapp:1.0.0
imagePullPolicy: Always
# Security context for container
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# Resource limits
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
# Liveness and readiness probes
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# Environment variables from secrets
envFrom:
- secretRef:
name: myapp-secrets
# Volume mounts for writable directories
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
# Network policy
# Defined separately below
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: myapp-netpol
namespace: production
spec:
podSelector:
matchLabels:
app: myapp
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- protocol: TCP
port: 3000
egress:
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
# Allow database
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Allow external HTTPS
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443
4. Security Scanning Script
#!/bin/bash
set -euo pipefail
echo "=== Security Hardening Validation ==="
# Check Dockerfile
echo ""
echo "Checking Dockerfile..."
hadolint Dockerfile || echo "⚠️ Dockerfile has issues"
# Check for secrets
echo ""
echo "Checking for secrets..."
gitleaks detect --source . --verbose || echo "⚠️ Potential secrets found"
# Check dependencies
echo ""
echo "Checking dependencies..."
npm audit --audit-level=high || echo "⚠️ Vulnerable dependencies found"
# Check container image
echo ""
echo "Scanning container image..."
trivy image --severity HIGH,CRITICAL myapp:latest || echo "⚠️ Vulnerabilities found"
# Check Kubernetes manifests
echo ""
echo "Checking Kubernetes config..."
kubesec scan k8s/*.yaml || echo "⚠️ Security issues in K8s config"
# Check for default passwords
echo ""
echo "Checking for default passwords..."
grep -r "password.*123\|admin.*admin" --exclude-dir=.git . && echo "❌ Default passwords found" || echo "✅ No default passwords"
# Check file permissions
echo ""
echo "Checking file permissions..."
find . -type f -perm 0777 -ls && echo "⚠️ World-writable files found" || echo "✅ No world-writable files"
# SSL/TLS check (if applicable)
echo ""
echo "Checking SSL/TLS..."
if command -v testssl &> /dev/null; then
testssl --severity HIGH https://example.com || echo "⚠️ SSL/TLS issues found"
fi
echo ""
echo "=== Security scan complete ==="
5. Security Checklist Script
#!/bin/bash
# security-checklist.sh
echo "Security Hardening Checklist"
echo "=============================="
echo ""
check_item() {
local description=$1
local command=$2
echo -n "[$description] "
if eval "$command" &>/dev/null; then
echo "✅"
return 0
else
echo "❌"
return 1
fi
}
# Dockerfile checks
echo "Dockerfile Security:"
check_item "Non-root user" "grep -q 'USER' Dockerfile && ! grep -q 'USER root' Dockerfile"
check_item "Specific base image tag" "! grep -q 'FROM.*:latest' Dockerfile"
check_item "Health check defined" "grep -q 'HEALTHCHECK' Dockerfile"
check_item "Minimal EXPOSE" "test $(grep -c 'EXPOSE' Dockerfile || echo 0) -le 2"
echo ""
echo "Container Security:"
check_item "Security scanning enabled" "command -v trivy"
check_item "No secrets in image" "! docker run --rm myapp:latest env | grep -i 'password\|secret\|key'"
check_item "Read-only filesystem" "grep -q 'readOnlyRootFilesystem: true' k8s/*.yaml"
echo ""
echo "Network Security:"
check_item "TLS configured" "grep -q 'ssl_protocols' nginx.conf"
check_item "Security headers" "grep -q 'X-Frame-Options' nginx.conf"
check_item "HSTS enabled" "grep -q 'Strict-Transport-Security' nginx.conf"
echo ""
echo "Access Control:"
check_item "No default credentials" "! grep -r 'password.*123\|admin.*admin' ."
check_item "Least privilege" "grep -q 'runAsNonRoot: true' k8s/*.yaml"
check_item "Network policy defined" "test -f k8s/networkpolicy.yaml"
echo ""
echo "Monitoring:"
check_item "Logging configured" "grep -q 'access_log' nginx.conf"
check_item "Health checks" "grep -q 'livenessProbe' k8s/*.yaml"
echo ""
echo "Dependencies:"
check_item "No critical vulnerabilities" "npm audit --audit-level=critical"
check_item "Updated packages" "test $(npm outdated | wc -l) -lt 5"
Explanation
Security Hardening Principles
1. Least Privilege
Start with nothing → Add only what's needed
- Run as non-root user
- Drop all capabilities
- Read-only filesystem
- Minimal network access
2. Defense in Depth
Multiple layers of security:
- Container security
- Network security
- Application security
- Infrastructure security
3. Zero Trust
Never trust, always verify:
- Authenticate everything
- Encrypt in transit
- Validate inputs
- Log access
Common Security Issues
| Issue | Risk | Fix |
|---|---|---|
| Running as root | Privilege escalation | Use non-root user |
| Default passwords | Unauthorized access | Strong unique passwords |
| No TLS | Data interception | Enable TLS 1.2+ |
| Missing headers | XSS, clickjacking | Add security headers |
| Open ports | Attack surface | Expose only needed ports |
Try to solve the challenge yourself first!
Click "Reveal Solution" when you're ready to see the answer.
Result
Apply Hardening
# Build hardened image
docker build -t myapp:hardened -f Dockerfile.hardened .
# Run with security options
docker run -d \
--name myapp \
--read-only \
--tmpfs /tmp \
--tmpfs /app/logs \
--security-opt=no-new-privileges:true \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-p 3000:3000 \
myapp:hardened
# Deploy hardened Kubernetes config
kubectl apply -f hardened-deployment.yaml
# Run security checks
./security-checklist.sh
Verify Security
# Check container runs as non-root
docker inspect myapp | jq '.[0].Config.User'
# Should not be "root" or empty
# Verify read-only filesystem
docker exec myapp touch /test
# Should fail
# Check capabilities
docker exec myapp capsh --print
# Should show minimal capabilities
# Test security headers
curl -I https://example.com
# Should include X-Frame-Options, etc.
Validation
Security Audit Checklist
# 1. No root user
docker inspect myapp | jq '.[0].Config.User' | grep -v "root"
# Should pass
# 2. Security headers present
curl -I https://example.com | grep -E "X-Frame-Options|Content-Security-Policy"
# Should show headers
# 3. TLS 1.2+ only
nmap --script ssl-enum-ciphers -p 443 example.com
# Should show TLS 1.2/1.3 only
# 4. No vulnerabilities
trivy image --severity CRITICAL myapp:hardened
# Should show 0 critical
# 5. Network policy active
kubectl get networkpolicy -n production
# Should list policy
# 6. Secrets not in environment
kubectl exec -n production myapp-xxx -- env | grep -i "password\|secret"
# Should be empty or from proper secrets
Best Practices
✅ Do's
- Run as non-root: Always
- Use specific versions: Not :latest
- Minimize attack surface: Remove unnecessary components
- Enable security features: Headers, TLS, etc.
- Regular updates: Patch vulnerabilities
- Audit regularly: Check configurations
❌ Don'ts
- Don't use default credentials: Change immediately
- Don't expose unnecessary ports: Minimal exposure
- Don't skip TLS: Always encrypt
- Don't ignore warnings: Fix security issues
- Don't trust input: Validate everything
Links
Share Your Success
Hardened your configs? Share what you did!
Tag @thedevopsdaily on X with:
- Security improvements made
- Before/after scan results
- Hardening checklist
- Lessons learned
Use hashtags: #AdventOfDevOps #Security #Hardening #Day24
Ready to complete this challenge?
Mark this challenge as complete once you've finished the task. We'll track your progress!
Completed this challenge? Share your success!
Tag @thedevopsdaily on X (Twitter) and share your learning journey with the community!
These amazing companies help us create free, high-quality DevOps content for the community
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?