D8.6 Πρακτικό παράδειγμα — Hardened Production Stack 🚀
Τι θα φτιάξουμε
Ένα πλήρες hardened stack που εφαρμόζει όλες τις security best practices:
---
# hardened-stack.yml
- name: Hardened Docker Stack
hosts: "{{ target | default('all_managed') }}"
become: true
gather_facts: true
vars:
stack: myapp
app_version: "{{ new_version | mandatory }}"
secrets_dir: /run/secrets/{{ stack }}
images:
nginx: "nginx:1.25.3-alpine"
app: "myapp:{{ app_version }}"
postgres: "postgres:15.4-alpine"
redis: "redis:7.2.3-alpine"
tasks:
# ════════════════════════════════════════
# PHASE 1: IMAGE SECURITY SCAN
# ════════════════════════════════════════
- name: "════ PHASE 1: Image Scan ════"
ansible.builtin.debug:
msg: "Scanning images για CVEs..."
- name: "🔍 Trivy scan: {{ item.value }}"
ansible.builtin.command:
cmd: >
trivy image
--exit-code 1
--severity HIGH,CRITICAL
--no-progress
"{{ item.value }}"
delegate_to: localhost
run_once: true
changed_when: false
loop: "{{ images | dict2items }}"
loop_control:
label: "{{ item.value }}"
ignore_errors: "{{ allow_vulnerabilities | default(false) }}"
# ════════════════════════════════════════
# PHASE 2: SECRETS
# ════════════════════════════════════════
- name: "════ PHASE 2: Secrets ════"
ansible.builtin.debug:
msg: "Ρύθμιση secrets..."
- name: "🔐 Secrets directory (700)"
ansible.builtin.file:
path: "{{ secrets_dir }}"
state: directory
owner: root
group: root
mode: '0700'
- name: "🔐 Write secrets"
ansible.builtin.copy:
content: "{{ item.value }}"
dest: "{{ secrets_dir }}/{{ item.key }}"
owner: root
group: root
mode: '0600'
loop:
- { key: postgres_pass, value: "{{ vault_postgres_pass }}" }
- { key: redis_pass, value: "{{ vault_redis_pass }}" }
- { key: app_secret, value: "{{ vault_app_secret }}" }
loop_control:
label: "{{ item.key }}"
no_log: true
# ════════════════════════════════════════
# PHASE 3: NETWORKS
# ════════════════════════════════════════
- name: "════ PHASE 3: Networks ════"
ansible.builtin.debug:
msg: "Δημιουργία isolated networks..."
- name: "🌐 Frontend network"
community.docker.docker_network:
name: "{{ stack }}_frontend"
state: present
labels:
stack: "{{ stack }}"
security: hardened
- name: "🌐 Backend network (internal)"
community.docker.docker_network:
name: "{{ stack }}_backend"
state: present
internal: true # ← NO internet!
labels:
stack: "{{ stack }}"
security: hardened
# ════════════════════════════════════════
# PHASE 4: CONTAINERS
# ════════════════════════════════════════
- name: "════ PHASE 4: Containers ════"
ansible.builtin.debug:
msg: "Deployment hardened containers..."
# ── PostgreSQL ────────────────────────────
- name: "🐘 PostgreSQL (hardened)"
community.docker.docker_container:
name: "{{ stack }}_postgres"
image: "{{ images.postgres }}"
state: started
restart_policy: unless-stopped
user: "999:999" # ← postgres user
read_only: true
memory: "512m"
pids_limit: 100
cap_drop: [ALL]
cap_add: [CHOWN, FOWNER, SETGID, SETUID]
security_opts: ["no-new-privileges:true"]
tmpfs:
/tmp: "size=64m"
/var/run/postgresql: "size=16m"
networks:
- name: "{{ stack }}_backend" # ← backend ΜΟΝΟ
volumes:
- "{{ stack }}_postgres:/var/lib/postgresql/data:rw"
- "{{ secrets_dir }}/postgres_pass:/run/secrets/postgres_password:ro"
env:
POSTGRES_PASSWORD_FILE: "/run/secrets/postgres_password"
POSTGRES_USER: appuser
POSTGRES_DB: mydb
log_driver: json-file
log_options:
max-size: "10m"
max-file: "3"
labels:
stack: "{{ stack }}"
security: hardened
# ── Redis ─────────────────────────────────
- name: "📮 Redis (hardened)"
community.docker.docker_container:
name: "{{ stack }}_redis"
image: "{{ images.redis }}"
state: started
restart_policy: unless-stopped
user: "999:999"
read_only: true
memory: "256m"
pids_limit: 50
cap_drop: [ALL]
security_opts: ["no-new-privileges:true"]
tmpfs:
/tmp: "size=32m"
networks:
- name: "{{ stack }}_backend"
volumes:
- "{{ stack }}_redis:/data:rw"
- "{{ secrets_dir }}/redis_pass:/run/secrets/redis_password:ro"
command: >
sh -c "redis-server
--requirepass $(cat /run/secrets/redis_password)
--maxmemory 200mb
--maxmemory-policy allkeys-lru"
log_driver: json-file
log_options:
max-size: "5m"
max-file: "2"
labels:
stack: "{{ stack }}"
security: hardened
# ── App ───────────────────────────────────
- name: "🚀 App (hardened)"
community.docker.docker_container:
name: "{{ stack }}_app"
image: "{{ images.app }}"
state: started
restart_policy: unless-stopped
user: "1000:1000"
read_only: true
memory: "512m"
pids_limit: 200
cap_drop: [ALL]
security_opts: ["no-new-privileges:true"]
tmpfs:
/tmp: "size=64m"
/app/tmp: "size=64m"
/app/cache: "size=128m"
networks:
- name: "{{ stack }}_frontend"
- name: "{{ stack }}_backend"
volumes:
- "{{ stack }}_uploads:/app/uploads:rw"
- "{{ secrets_dir }}/app_secret:/run/secrets/app_secret:ro"
env:
NODE_ENV: production
APP_VERSION: "{{ app_version }}"
SECRET_FILE: "/run/secrets/app_secret"
DATABASE_URL: "postgresql://appuser@{{ stack }}_postgres:5432/mydb?sslmode=disable"
REDIS_URL: "redis://{{ stack }}_redis:6379"
log_driver: json-file
log_options:
max-size: "20m"
max-file: "5"
labels:
stack: "{{ stack }}"
version: "{{ app_version }}"
security: hardened
# ── Nginx ─────────────────────────────────
- name: "🌐 Nginx (hardened)"
community.docker.docker_container:
name: "{{ stack }}_nginx"
image: "{{ images.nginx }}"
state: started
restart_policy: unless-stopped
user: "101:101" # ← nginx user
read_only: true
memory: "128m"
pids_limit: 50
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE] # ← port 80/443
security_opts: ["no-new-privileges:true"]
tmpfs:
/tmp: "size=32m"
/var/cache/nginx: "size=128m"
/var/run: "size=16m"
ports:
- "127.0.0.1:80:80" # ← localhost ΜΟΝΟ
- "127.0.0.1:443:443" # ← reverse proxy το εκθέτει
networks:
- name: "{{ stack }}_frontend" # ← frontend ΜΟΝΟ
volumes:
- "/etc/nginx/{{ stack }}.conf:/etc/nginx/nginx.conf:ro"
- "/etc/ssl/certs:/etc/ssl/certs:ro"
- "{{ stack }}_uploads:/app/uploads:ro" # ← static files
log_driver: json-file
log_options:
max-size: "20m"
max-file: "5"
labels:
stack: "{{ stack }}"
security: hardened
# ════════════════════════════════════════
# PHASE 5: VERIFICATION
# ════════════════════════════════════════
- name: "🔍 Security audit"
ansible.builtin.command:
cmd: >
docker inspect
{{ stack }}_postgres
{{ stack }}_redis
{{ stack }}_app
{{ stack }}_nginx
--format "{{ '{{' }}.Name{{ '}}' }}: ReadOnly={{ '{{' }}.HostConfig.ReadonlyRootfs{{ '}}' }}, User={{ '{{' }}.Config.User{{ '}}' }}, NoNewPriv={{ '{{' }}.HostConfig.SecurityOpt{{ '}}' }}"
register: audit
changed_when: false
- name: "📊 Security Report"
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════════════╗"
- "║ 🔒 HARDENED STACK SECURITY REPORT ║"
- "╠══════════════════════════════════════════════╣"
- "{{ audit.stdout_lines }}"
- "╠══════════════════════════════════════════════╣"
- "║ ✅ All containers: non-root ║"
- "║ ✅ All containers: read-only filesystem ║"
- "║ ✅ All containers: no-new-privileges ║"
- "║ ✅ Secrets: file mounts (not env vars) ║"
- "║ ✅ Networks: backend isolated (no internet) ║"
- "╚══════════════════════════════════════════════╝"
# Εκτέλεση
ansible-playbook playbooks/hardened-stack.yml \
--limit nextcloud \
-e "new_version=2.5.0" \
--ask-vault-pass
# Verify security settings
ansible nextcloud -m ansible.builtin.command \
-a "docker inspect myapp_app --format '{{.HostConfig.ReadonlyRootfs}}'" \
--become
# → true ✅
Σύνοψη D8.6
Hardened Stack — Security layers:
│
├── Image: Trivy scan πριν deployment
├── Secrets: Vault + file mounts
├── Network: frontend/backend isolation
└── Runtime (κάθε container):
├── user: non-root (999 ή 1000)
├── read_only: true
├── cap_drop: [ALL]
├── security_opts: [no-new-privileges]
├── tmpfs: για writable dirs
└── pids_limit: για DoS protection