Rootless containers με Podman και Quadlet: υπηρεσίες χωρίς root daemon

Για χρόνια, το «τρέχω containers» σήμαινε ένα δαίμονα (daemon) που τρέχει ως root, με ένα socket που όποιος το αγγίξει αποκτά ουσιαστικά root στο μηχάνημα. Το Podman ανατρέπει αυτό το μοντέλο: χωρίς δαίμονα, με containers που τρέχουν ως απλός χρήστης, και με ενσωμάτωση στο systemd μέσω των Quadlet. Ας δούμε γιατί αυτό είναι ουσιαστική βελτίωση ασφάλειας, και πώς στήνεται μια πραγματική υπηρεσία.

Το πρόβλημα του daemon

Στο κλασικό μοντέλο, ένας κεντρικός δαίμονας ως root εκτελεί όλα τα containers. Δύο συνέπειες: όποιος έχει πρόσβαση στο socket του δαίμονα μπορεί να ξεκινήσει ένα container που κάνει mount ολόκληρο το host filesystem και να αποδράσει σε root· και μια ευπάθεια στον ίδιο τον δαίμονα είναι ευπάθεια με δικαιώματα root. Είναι ένα single point of failure με τα υψηλότερα δυνατά προνόμια.

Πώς το λύνει το Podman

  • Χωρίς δαίμονα (daemonless): κάθε container είναι απλώς ένα παιδί της εντολής που το ξεκίνησε. Δεν υπάρχει μόνιμη προνομιούχα διεργασία να παραβιαστεί.

  • Rootless: το container τρέχει ως ο κανονικός σας χρήστης. Μέσω των user namespaces, ο root *μέσα* στο container αντιστοιχίζεται σε έναν μη προνομιούχο χρήστη *έξω*. Αν κάτι αποδράσει, βρίσκεται ως ακίνδυνος χρήστης, όχι ως root στο host.

  • Συμβατό με το Docker: το CLI είναι σχεδόν ταυτόσημο (alias docker=podman δουλεύει στις περισσότερες περιπτώσεις), και διαβάζει τα ίδια images από τα ίδια registries.

Το κόστος: το rootless networking περνά από έναν user-space μηχανισμό (pasta ή slirp4netns), με ελαφρώς μικρότερη απόδοση δικτύου, και τα privileged ports (κάτω από 1024) χρειάζονται ρητή ρύθμιση. Για τις περισσότερες υπηρεσίες πίσω από έναν reverse proxy, κανένα από τα δύο δεν είναι πρόβλημα.

Το παλιό αντιπρότυπο: podman generate systemd

Παλιότερα, η σύνδεση με το systemd γινόταν με podman generate systemd, που παρήγαγε ένα unit file. Το αντιπρότυπο ήταν ότι το παραγόμενο αρχείο «πάγωνε»: κάθε αλλαγή απαιτούσε να το ξαναπαράγετε. Αυτή η εντολή θεωρείται πλέον ξεπερασμένη, υπέρ των Quadlet.

Το σύγχρονο μοντέλο: Quadlet

Με το Quadlet, περιγράφετε το container δηλωτικά σε ένα αρχείο .container, και το systemd παράγει το unit αυτόματα. Παράδειγμα, στο ~/.config/containers/systemd/myapp.container για rootless υπηρεσία:

[Unit]

Description=My application

After=network-online.target

[Container]

Image=ghcr.io/example/myapp:1.4.2

PublishPort=8080:8080

Volume=myapp-data:/data:Z

Environment=TZ=Europe/Athens

# Σκλήρυνση:

NoNewPrivileges=true

DropCapability=ALL

ReadOnly=true

[Service]

Restart=always

[Install]

WantedBy=default.target

Μετά, systemctl --user daemon-reload και systemctl --user start myapp. Το systemd αναλαμβάνει εκκίνηση, επανεκκίνηση, logs (journalctl --user -u myapp) και εξαρτήσεις, όπως για κάθε άλλη υπηρεσία.

Δύο σημαντικές λεπτομέρειες για rootless υπηρεσίες που πρέπει να «ζουν» χωρίς ενεργή σύνδεση του χρήστη: ενεργοποιήστε το lingering με loginctl enable-linger $USER, ώστε οι υπηρεσίες του χρήστη να ξεκινούν στο boot· και το :Z στα volumes ρυθμίζει αυτόματα τις ετικέτες SELinux.

Αυτόματες ενημερώσεις

Το Podman συνοδεύεται από έτοιμο μηχανισμό ενημερώσεων. Προσθέτοντας AutoUpdate=registry στο [Container] και ενεργοποιώντας το timer podman-auto-update.timer, το σύστημα ελέγχει περιοδικά για νεότερο image και επανεκκινεί την υπηρεσία.

Το Podman κάνει αυτόματο rollback στο προηγούμενο image αν η επανεκκίνηση του unit αποτύχει μετά την ενημέρωση. Για να είναι αξιόπιστο, το systemd πρέπει να μπορεί να ξεχωρίσει το «ξεκίνησε» από το «είναι έτοιμο»: αυτό γίνεται με σήμα readiness. Στο Quadlet, προσθέτετε Notify=healthy (η ετοιμότητα δένεται στον healthcheck) ή Notify=true (η εφαρμογή στέλνει μόνη της READY=1 μέσω sdnotify), μαζί με Type=notify στο [Service] και έναν HealthCmd. Παράδειγμα προσθήκης στο προηγούμενο αρχείο:

[Container]

# …όπως πριν…

AutoUpdate=registry

HealthCmd=curl -f http://localhost:8080/health || exit 1

HealthInterval=30s

HealthStartPeriod=60s

Notify=healthy

[Service]

Type=notify

TimeoutStartSec=120

Έτσι, αν η νέα έκδοση δεν γίνει «healthy» μέσα στο χρονικό όριο, το Podman επαναφέρει την προηγούμενη, που δούλευε.

Σκλήρυνση με λίγα επιπλέον βήματα

  • `NoNewPrivileges=true` και `DropCapability=ALL`, προσθέτοντας μόνο όσα capabilities χρειάζεται πραγματικά η εφαρμογή.

  • `ReadOnly=true` για το root filesystem του container, με ρητά writable volumes μόνο όπου χρειάζεται.

  • Μη προνομιούχος χρήστης μέσα στο container (User= ή κατάλληλο image), ώστε, ακόμη και χωρίς user namespace, να μην τρέχει ως root.

  • Περιορισμοί πόρων (PidsLimit, memory) για να μην μπορεί ένα container να «πνίξει» το host.

Για συζήτηση

  1. Έχετε μεταφέρει υπηρεσίες σε rootless Podman με Quadlet; Τι σας δυσκόλεψε περισσότερο, το networking ή τα permissions στα volumes;

  2. Εμπιστεύεστε το AutoUpdate=registry σε production, ή προτιμάτε ελεγχόμενες ενημερώσεις;

  3. Για ποια φορτία θεωρείτε ότι το rootless δεν αρκεί ακόμη, και γιατί;