D8.2 Container Runtime Security ⚙️

Το πρόβλημα με τα defaults

# Default container — εντελώς ανοιχτό:
docker run nginx

# Τρέχει ως root UID 0         ❌
# Μπορεί να κάνει setuid       ❌
# Filesystem εγγράψιμο         ❌
# Έχει ~14 capabilities        ❌

no-new-privileges — Αποφυγή privilege escalation

tasks:

  - name: Container με no-new-privileges
    community.docker.docker_container:
      name:            myapp
      image:           myapp:latest
      state:           started
      security_opts:
        - "no-new-privileges:true"    # ← ΔΕΝ μπορεί να αποκτήσει νέα privileges
                                      #   μέσω setuid/setgid binaries

💡 Τι κάνει: Ακόμα κι αν υπάρχει setuid binary μέσα στο container, δεν θα εκτελεστεί με elevated privileges.


read_only — Read-only filesystem

tasks:

  - name: Read-only container
    community.docker.docker_container:
      name:    myapp
      image:   myapp:latest
      state:   started
      read_only: true           # ← filesystem μόνο για ανάγνωση!

      # Αλλά χρειαζόμαστε κάποια writable directories:
      tmpfs:
        /tmp:   "size=128m,mode=1777"      # ← temp files
        /run:   "size=32m"                  # ← PID files, sockets
        /app/cache: "size=64m"             # ← app cache

      volumes:
        # Μόνο ό,τι ΠΡΕΠΕΙ να είναι writable
        - "app_data:/app/data:rw"
        - "/etc/myapp/config:/app/config:ro"    # ← config: read-only!

⚠️ Συνηθισμένη παγίδα: Πολλά apps γράφουν σε /tmp ή /run. Με read_only: true, αυτά αποτυγχάνουν αν δεν ορίσεις tmpfs.


Non-root user

tasks:

  # ── Εκτέλεση ως συγκεκριμένος user ──────────
  - name: Non-root container
    community.docker.docker_container:
      name:  myapp
      image: myapp:latest
      state: started
      user:  "1000:1000"    # ← UID:GID (όχι root=0)
      # ή
      user:  "myapp"         # ← username (πρέπει να υπάρχει στο image)

  # ── Volume permissions (πριν το container) ───
  - name: Σωστά permissions για non-root
    ansible.builtin.file:
      path:  /opt/myapp/data
      state: directory
      owner: "1000"
      group: "1000"
      mode:  '0750'

  - name: Deploy container
    community.docker.docker_container:
      name:    myapp
      image:   myapp:latest
      state:   started
      user:    "1000:1000"
      volumes:
        - "/opt/myapp/data:/app/data:rw"

Capabilities — Minimal permissions

tasks:

  # ── Drop ALL capabilities ─────────────────
  - name: Container χωρίς capabilities
    community.docker.docker_container:
      name:           myapp
      image:          myapp:latest
      state:          started
      capabilities:   []              # ← ΚΑΝΕΝΑ capability
      cap_drop:
        - ALL                         # ← ρητό drop all

  # ── Προσθήκη μόνο ό,τι χρειάζεται ────────
  - name: Nginx με minimal capabilities
    community.docker.docker_container:
      name:  nginx_secure
      image: nginx:alpine
      state: started
      cap_drop:
        - ALL
      cap_add:
        - NET_BIND_SERVICE    # ← μόνο για ports < 1024
      # ΌΛΑ τα άλλα dropped:
      # No: SYS_ADMIN, SYS_PTRACE, NET_RAW, etc.

  # ── Common capabilities per service type ──
  # Web server:  NET_BIND_SERVICE
  # Database:    IPC_LOCK (για shared memory)
  # Monitoring:  SYS_PTRACE (για process info)

seccomp — System call filtering

tasks:

  # ── Default Docker seccomp profile ────────
  - name: Container με default seccomp
    community.docker.docker_container:
      name:  myapp
      image: myapp:latest
      state: started
      # default seccomp εφαρμόζεται αυτόματα
      # (μπλοκάρει ~44 επικίνδυνα syscalls)

  # ── Custom seccomp profile ─────────────────
  - name: Container με custom seccomp
    community.docker.docker_container:
      name:  myapp
      image: myapp:latest
      state: started
      security_opts:
        - "no-new-privileges:true"
        - "seccomp={{ lookup('file', 'files/seccomp/myapp-profile.json') | to_json }}"

Πλήρης hardened container

tasks:

  - name: "🔒 Hardened nginx container"
    community.docker.docker_container:
      name:            nginx_hardened
      image:           nginx:1.25-alpine
      state:           started
      restart_policy:  unless-stopped

      # ── User ────────────────────────────────
      user: "101:101"              # ← nginx user (non-root)

      # ── Filesystem ──────────────────────────
      read_only: true
      tmpfs:
        /tmp:  "size=32m,mode=1777"
        /var/cache/nginx: "size=128m"
        /var/run:         "size=16m"

      # ── Capabilities ────────────────────────
      cap_drop:
        - ALL
      cap_add:
        - NET_BIND_SERVICE    # ← port 80/443

      # ── Privileges ──────────────────────────
      security_opts:
        - "no-new-privileges:true"

      # ── Resources ───────────────────────────
      memory:      "128m"
      cpus:        "0.5"
      pids_limit:  50             # ← max 50 processes

      # ── Ports ───────────────────────────────
      ports:
        - "127.0.0.1:80:80"      # ← μόνο localhost (πίσω από reverse proxy)
        # ΌΧΙ: "80:80" (expose σε όλες τις interfaces)

      # ── Volumes ─────────────────────────────
      volumes:
        - "/etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
        - "/var/www/html:/usr/share/nginx/html:ro"
        - "/etc/ssl:/etc/ssl:ro"

      # ── Logging ─────────────────────────────
      log_driver: json-file
      log_options:
        max-size: "10m"
        max-file: "3"

      # ── Labels ──────────────────────────────
      labels:
        security: hardened
        managed-by: ansible

Σύνοψη D8.2

Container Runtime Security
│
├── no-new-privileges:
│   └── security_opts: ["no-new-privileges:true"]
│
├── read_only + tmpfs:
│   ├── read_only: true
│   └── tmpfs: {/tmp: "size=128m", /run: "size=32m"}
│
├── Non-root user:
│   └── user: "1000:1000"
│
├── Capabilities:
│   ├── cap_drop: [ALL]
│   └── cap_add: [NET_BIND_SERVICE] ← μόνο ό,τι χρειάζεται
│
└── Resources:
    ├── memory: "128m"
    ├── cpus: "0.5"
    └── pids_limit: 50