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