Ένα πάνελ διαχείρισης σε διακομιστή προσκρούει σχεδόν πάντα στον ίδιο τοίχο: η διεργασία του web πρέπει να κάνει κάτι για το οποίο δεν έχει δικαιώματα. Να αποκλείσει μια διεύθυνση, να ανοίξει μια θύρα, να διαβάσει το ημερολόγιο του συστήματος. Η απάντηση που έρχεται αμέσως στο μυαλό είναι να δοθεί μια μόνο στενή γραμμή στο sudoers και τίποτε άλλο:

www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client set * banip *

Η γραμμή διαβάζεται ως «μόνο αποκλεισμός». Στην πραγματικότητα σημαίνει «οτιδήποτε, ως root». Παρακάτω: γιατί βγαίνει έτσι, πώς το ελέγχετε στον δικό σας διακομιστή σε πέντε λεπτά και με τι αντικαθίστανται τέτοιοι κανόνες.

Τι κάνει στ’ αλήθεια ο *

Η κρίσιμη λεπτομέρεια: το sudo δεν συγκρίνει τα ορίσματα ένα προς ένα αλλά τη γραμμή εντολών ως σύνολο, και ο * στο πρότυπο περνά άνετα πάνω από τα διαστήματα — δηλαδή πάνω από τα σύνορα των ορισμάτων. Η κυριολεκτική λέξη banip που στέκεται στη μέση του κανόνα δεν περιορίζει επομένως τίποτα: αρκεί η λέξη banip να εμφανιστεί κάπου στην εντολή, και πριν από αυτήν όπως και μετά μπορεί να περαστεί οτιδήποτε.

Στη συνέχεια αναζητείται ένα πρόγραμμα στο οποίο μπορεί να δοθεί εντολή προς εκτέλεση. Στην περίπτωσή μας βρισκόταν μέσα στον ίδιο τον κανόνα. Στο fail2ban η ενέργεια αποκλεισμού ορίζεται ως κείμενο, και αυτό το κείμενο μπορεί να επανοριστεί εν λειτουργία:

fail2ban-client get portscan actions
sudo fail2ban-client set portscan action nftables-multiport actionban "<εντολή>" banip 192.0.2.10
sudo fail2ban-client set portscan banip 192.0.2.11

Η πρώτη κλήση αντικαθιστά το κείμενο ενέργειας σε μια πραγματική ενέργεια, η δεύτερη κάνει το jail να ενεργοποιηθεί — και το fail2ban, που εκτελείται ως root, εκτελεί ό,τι παρεμβλήθηκε. Το κάναμε στις 4 Ιουλίου στον δικό μας διακομιστή: ένα αρχείο στο /root δημιουργήθηκε με δικαιώματα root, δηλαδή η διεργασία του web απέκτησε πλήρη δικαιώματα στη μηχανή από έναν κανόνα που έμοιαζε στενός.

Στην πορεία βγήκαν τρεις λεπτομέρειες που στις ξένες αναλύσεις συνήθως λείπουν:

  • τον διακόπτη -c πριν από το set τον κόβει το sudo: στο πρότυπο το set είναι κολλημένο απευθείας στο όνομα του εκτελέσιμου και πριν από αυτό δεν μπορεί να παρεμβληθεί τίποτα·
  • τα addaction και action χωρίς τη λέξη banip επίσης δεν περνούν — αλλά όλα τα απαραίτητα χωρούν σε μια μόνο εντολή set … banip …, και έτσι το πρότυπο ικανοποιείται·
  • το όνομα της ενέργειας πρέπει να είναι πραγματικό, αλλιώς δεν υπάρχει τι να αντικατασταθεί: το δείχνει το fail2ban-client get <jail> actions.

Ποιοι άλλοι βρίσκονται στην ίδια λίστα

Το fail2ban δεν φταίει εδώ ούτε είναι μοναδική περίπτωση. Επικίνδυνος είναι ο ίδιος ο συνδυασμός NOPASSWD με αστερίσκο. Τι βρήκαμε δίπλα του στους δικούς μας διακομιστές:

  • journalctl * — το ημερολόγιο ανοίγει μέσω σελιδοποιητή, και από τον σελιδοποιητή εκκινείται κέλυφος. Ο κανόνας μοιάζει να αφορά την ανάγνωση αρχείων καταγραφής· στην πράξη είναι κέλυφος root. Η θεραπεία δεν είναι στενότερο πρότυπο αλλά η ομάδα systemd-journal: τότε το journalctl λειτουργεί εντελώς χωρίς sudo·
  • grep * /var/log/fail2ban.log — το πρώτο όρισμα του grep είναι το πρότυπο, αλλά ο αστερίσκος επιτρέπει να δοθεί και δεύτερη διαδρομή, και το grep εκτελείται ως root. Η ανάγνωση του /etc/shadow με αυτόν τον κανόνα είναι μία εντολή. Η αντικατάσταση είναι η ίδια: η ομάδα adm για την ανάγνωση των αρχείων καταγραφής·
  • ufw --force * και τα γυμνά ufw allow/deny/delete * — η διεργασία του web μπορεί να απενεργοποιήσει εντελώς το τείχος προστασίας. Ιδιαίτερα δυσάρεστο: και στις δύο περιπτώσεις το πάνελ δεν χρησιμοποιούσε καθόλου αυτούς τους κανόνες — νεκρά δικαιώματα που έμειναν από πρώιμες εκδόσεις του προγράμματος εγκατάστασης·
  • lynis-scan.sh * — ένα περικάλυμμα που προωθεί το "$@" σε διεργασία root ισοδυναμεί με κανόνα χωρίς κανέναν περιορισμό.

Πώς βλέπετε τι υπάρχει σε εσάς

Αυτό που πρέπει να κοιταχτεί δεν είναι το αρχείο αλλά τα πραγματικά δικαιώματα ενός συγκεκριμένου χρήστη — εκείνου με τον οποίο εκτελείται όντως η δεξαμενή PHP-FPM (όχι πάντα ο www-data· σε μια από τις μηχανές μας το πάνελ εκτελούνταν ως admin):

ps -o user= -C php-fpm | sort -u
sudo -l -U www-data
sudo -l -U admin

Έπειτα η ίδια η λίστα των αρχείων, και εδώ υπάρχουν δύο παγίδες που μας κόστισαν χρόνο:

sudo grep -rn 'NOPASSWD' /etc/sudoers /etc/sudoers.d/
ls -la /etc/sudoers.d/

Η πρώτη: οι κανόνες ζουν σε δύο σημεία ταυτόχρονα. Σε εμάς η επικίνδυνη γραμμή βρισκόταν και στο /etc/sudoers.d/monitor και στο κύριο /etc/sudoers — στο κύριο σε περίπου οκτώ αντίγραφα, συσσωρευμένα από διαφορετικές εκδόσεις του προγράμματος εγκατάστασης. Να καθαρίσει κανείς ένα σημείο και να ησυχάσει είναι ο συνηθισμένος τρόπος να μείνει η τρύπα ανοιχτή.

Η δεύτερη: το sudo αγνοεί τα αρχεία που έχουν τελεία στο όνομα. Ένα αρχείο www-data.bak3 στο /etc/sudoers.d/ μοιάζει με ισχύοντα κανόνα και διαβάζεται ως ισχύων κανόνας, αλλά δεν έχει καμία ισχύ. Αυτό δουλεύει και προς τις δύο κατευθύνσεις: «ο κανόνας υπάρχει, τα δικαιώματα όχι» και η ψεύτικη ησυχία ότι ένα αντίγραφο της ρύθμισης «βρίσκεται ακριβώς δίπλα».

Με τι αντικαθίσταται

Η στένωση του προτύπου δεν ωφελεί — ένας αστερίσκος οπουδήποτε στη γραμμή επαναφέρει το πρόβλημα στην αρχή. Αντέχει μία μόνο προσέγγιση: ένα περικάλυμμα root που δέχεται σταθερά ορίσματα θέσης, τα επικυρώνει το ίδιο και καλεί το πρόγραμμα χωρίς την παραμικρή δυνατότητα να προστεθεί κάτι.

#!/bin/sh
# /usr/local/sbin/arciveo-f2b — ban|unban <jail> <ip>, και τίποτε άλλο
set -eu
action="$1"; jail="$2"; ip="$3"
case "$action" in ban|unban) ;; *) echo "invalid action"; exit 1 ;; esac
case "$jail" in *[!a-zA-Z0-9_-]*|'') echo "invalid jail"; exit 1 ;; esac
printf '%s' "$ip" | grep -Eq '^[0-9a-fA-F:.]+$' || { echo "invalid ip"; exit 1; }

case "$action" in
  ban)   exec /usr/bin/fail2ban-client set "$jail" banip   "$ip" ;;
  unban) exec /usr/bin/fail2ban-client set "$jail" unbanip "$ip" ;;
esac

Το περικάλυμμα ανήκει στον root, δικαιώματα 755, και — αυτό δεν είναι προαιρετικό — δεν πρέπει να είναι εγγράψιμο από το web, αλλιώς όλη η κατασκευή χάνει το νόημά της. Στο sudoers μένει μόνο αυτό:

www-data ALL=(root) NOPASSWD: /usr/local/sbin/arciveo-f2b

Τα ορίσματα δεν απαριθμούνται στον κανόνα: τα ελέγχει το ίδιο το σκριπτ, και οι θέσεις μέσα του είναι σταθερές, οπότε δεν υπάρχει πού να παρεισφρήσει κείμενο ενέργειας. Οι κανόνες μόνο για ανάγνωση — καταστάσεις, ss, ipset list — μπαίνουν σε χωριστές γραμμές, επίσης χωρίς αστερίσκο όπου αυτό είναι εφικτό.

Χωριστά, για την αντικατάσταση του sudo με ομάδες. Η προσέγγιση είναι σωστή: η systemd-journal για το ημερολόγιο και η adm για τα αρχεία καταγραφής βγαίνουν φθηνότερες και ασφαλέστερες από κάθε κανόνα sudo. Όμως δεν μπορεί κανείς να βγάλει τον χρήστη του web από ομάδες στα τυφλά. Βγάλαμε τον www-data από μία ομάδα σε έναν κόμβο με πάνελ και πήραμε αμέσως 403 σε όλους τους ιστότοπους: ο Apache εκεί εκτελείται ως www-data, τα αρχεία των ιστότοπων ανήκουν σε άλλον χρήστη, και την πρόσβαση ανάγνωσης την έδινε ακριβώς εκείνη η συμμετοχή στην ομάδα. Η αποκατάσταση απαίτησε πλήρη επανεκκίνηση — όχι επαναφόρτωση — του Apache και του PHP-FPM, επειδή οι παλιές εργάτριες διεργασίες κρατούν το προηγούμενο σύνολο ομάδων και παράγουν την κατάσταση «δουλεύει, και μετά 403».

Έλεγχος μετά την αλλαγή

Πριν αντικατασταθεί ένα αρχείο sudoers πρέπει να ελεγχθεί η σύνταξή του — αλλιώς μπορεί να μείνει κανείς εντελώς χωρίς sudo:

sudo visudo -cf /etc/sudoers.d/monitor

Έπειτα πρέπει να επιβεβαιωθεί ότι το παλιό διάνυσμα είναι νεκρό, και μάλιστα στο όνομα εκείνου ακριβώς του χρήστη:

sudo -k
sudo -u www-data sudo -n fail2ban-client set portscan banip 192.0.2.10

Η απάντηση πρέπει να είναι a password is required, ενώ η κλήση του περικαλύμματος με σκουπίδια στη θέση διεύθυνσης πρέπει να δώσει invalid ip. Εδώ παραμονεύει μια δική της παγίδα: το sudo -v κρατά την επιβεβαίωση στην προσωρινή μνήμη για δεκαπέντε λεπτά, και μετά από αυτό κάθε έλεγχος με sudo -n περνά «με επιτυχία». Έτσι ακριβώς επιβεβαιώσαμε κάποτε έναν κανόνα που στον διακομιστή δεν υπήρχε καθόλου. Γι’ αυτό το sudo -k πριν από τον έλεγχο είναι υποχρεωτικό.

Σε τι χρησιμεύει η παρακολούθηση

Οι κανόνες του sudo αλλάζουν σπάνια, αλλά αλλάζουν αθόρυβα: το πρόγραμμα εγκατάστασης προσθέτει, το πάνελ της φιλοξενίας συμπληρώνει τα δικά του, ένα αφαιρεμένο πακέτο αφήνει πίσω του τις γραμμές του. Μια εφάπαξ επιθεώρηση κλείνει ό,τι υπάρχει σήμερα και δεν λέει τίποτα για ό,τι θα φέρει η επόμενη ενημέρωση. Το πρακτικό συμπέρασμα είναι απλό: η λίστα εκείνων που μπορούν να γίνουν root ανήκει σε εμφανές σημείο, δίπλα στην υπόλοιπη κατάσταση του διακομιστή, και όχι στη μνήμη μετά από ένα περιστατικό. Πώς φαίνεται αυτό συγκεντρωμένο το δείχνουν οι σελίδες επίδειξης παρακάτω.