Zum Inhalt

Podman

Alle Anwendungen auf unseren Hosts laufen als Container, betrieben von Podman. Diese Seite erklärt die Begriffe, die man zum Lesen einer Stack-Datei braucht, wie Podman und systemd die Container bei uns betreiben und wie Secrets in die Container kommen. Wie die Secrets verschlüsselt im Repo liegen, steht in Secrets im Repo.

Viele Apps auf einem Host

Auf einem Host laufen bei uns viele Apps, und jede bringt ihre eigenen Anforderungen mit:

  • Jede braucht bestimmte Bibliotheken, Laufzeitumgebungen und Dienste in bestimmten Versionen; direkt auf dem Host kommen sie sich in die Quere.
  • Updates und das Entfernen einer App hinterlassen Reste auf dem Host.
  • Eine App soll sich auf einem anderen Host genauso wieder aufsetzen lassen.

Container lösen das: Jede App läuft mit allem, was sie braucht, abgeschottet in ihrem eigenen Container. Damit die Container laufen, braucht es ein Programm, das sie startet und verwaltet.

Alle Apps laufen als Container, nicht direkt auf dem Host.

  • Jede App bringt im Container ihre eigenen Bibliotheken und Programme in der Version mit, die sie braucht. Zwei Apps, die verschiedene Versionen von PHP oder Postgres brauchen, kommen sich nicht in die Quere, und eine App sieht die Dateien der anderen nicht.
  • Ein Update tauscht nur das Image aus: Der alte Container wird entfernt, ein neuer aus dem neuen Image erzeugt. Auf dem Host bleibt nichts von der alten Version zurück, und ein Schritt zurück heißt, wieder das alte Image zu nehmen (solange die Daten das zulassen, siehe Image-Version anheben).
  • Ein Container aus demselben Image mit derselben Konfiguration verhält sich auf jedem Host gleich. Zusammen mit den Stack-Dateien im Repo lässt sich eine App deshalb jederzeit neu aufsetzen; nur die Daten in den Volumes kommen aus dem Backup.
  • Wird eine App nicht mehr gebraucht, verschwinden mit ihren Containern auch alle ihre Programme vom Host.

Die Container betreiben wir mit Podman. Es gibt andere Programme dafür; das bekannteste ist Docker, und viele Hersteller beschreiben ihre Apps für Docker. Podman ist weitgehend kompatibel mit Docker, gehört bei Rocky Linux zum Betriebssystem (siehe Server-Technologien), lässt die Container von systemd betreiben (siehe Podman und systemd) und setzt die SELinux-Labels für die Container selbst (siehe SELinux).

Image, Container, Volume, Netzwerk

Ein Image ist eine Vorlage: die Anwendung mit allem, was sie zum Laufen braucht, vom Hersteller fertig zusammengestellt. Images liegen in einer Registry, etwa Docker Hub oder Quay, und tragen einen Tag, der die Version angibt. quay.io/hedgedoc/hedgedoc:1.12.0 ist das Image von HedgeDoc mit dem Tag 1.12.0.

Ein Container ist eine laufende Anwendung, erzeugt aus einem Image. Er hat ein eigenes Dateisystem, eigene Prozesse und eigene Netzwerkadressen und sieht vom Host und von anderen Containern nur, was man ihm ausdrücklich gibt. Wird der Container neu erzeugt, etwa bei einem Update, beginnt er wieder mit dem Inhalt des Images; alles, was er vorher in sein eigenes Dateisystem geschrieben hat, ist weg.

Daten, die bleiben sollen, liegen deshalb in einem Volume: einem Speicherort auf dem Host, den Podman verwaltet und in den Container einblendet. Ein Volume überdauert jedes Neuerzeugen des Containers. Seltener blenden wir statt eines Volumes einen Ordner des Hosts direkt ein, einen Bind-Mount.

Über ein Netzwerk erreichen sich Container untereinander, und zwar über ihren Namen: HedgeDoc erreicht seine Datenbank unter hedgedoc-postgres. Ein Container kann an mehreren Netzwerken hängen.

flowchart LR
    registry[("Registry<br>z. B. quay.io")]
    image["Image<br>hedgedoc:1.12.0"]
    container["Container<br>hedgedoc-server"]
    volume[("Volume<br>hedgedoc-server-uploads")]
    db["Container<br>hedgedoc-postgres"]

    registry -->|herunterladen| image
    image -->|erzeugen| container
    container <-->|liest und schreibt| volume
    container <-->|"Netzwerk hedgedoc"| db

All das beschreiben wir in den Stack-Dateien unter syslet/<host>/, zum Beispiel syslet/werner/stack-hedgedoc.cue. Wie man so eine Datei liest, steht in Eine Stack-Datei lesen.

Podman und systemd

Die Container betreibt Podman, und zwar über systemd, das auf jedem Linux-Host die Dienste startet und überwacht.

syslet schreibt für jeden Container, jedes Volume und jedes Netzwerk eine Quadlet-Datei auf den Host. Daraus erzeugt systemd je eine systemd-Unit, für einen Container mit seinem Namen, etwa hedgedoc-server.service. Startet systemd die Unit, erzeugt Podman den Container; systemd überwacht ihn dann wie jeden anderen Dienst: Stürzt er ab, startet systemd ihn neu, und nach einem Neustart des Hosts startet es alle Container wieder.

Deshalb steuert man einen Container auf dem Host mit systemctl über seine Unit und nicht mit podman start oder podman stop. Was man direkt mit Podman tut, bekommt systemd nicht mit: Einen so gestoppten Container startet systemd unter Umständen gleich wieder, und ein so gestarteter läuft außerhalb seiner Unit, ohne Überwachung. Was man auf dem Host überhaupt von Hand tun darf, steht in Was man von Hand darf.

Die Container betreibt systemd, kein eigener Dienst. Podman braucht keinen Dienst, der dauerhaft im Hintergrund läuft und die Container verwaltet; das übernimmt systemd, das ohnehin läuft. Status, Logs und Neustarts funktionieren für Container deshalb genauso wie für jeden anderen Dienst des Hosts.

Interne Netzwerke

Auf werner bekommt jede App mit mehreren Containern ein eigenes Netzwerk, das als intern markiert ist (Internal: "true", in der Stack-Datei ganz oben). An einem internen Netzwerk hängen nur die Container dieses einen Stacks; sie erreichen sich gegenseitig, aber nicht das Internet, und von außen sind sie nicht erreichbar.

Datenbanken, Caches und Suchdienste hängen nur an diesem internen Netzwerk. Der Container mit der eigentlichen App hängt zusätzlich am Netzwerk caddy, über das ihn der Reverse Proxy erreicht.

Auf containerhost hängen dagegen alle Container an einem gemeinsamen Netzwerk; es heißt zwar internal, ist aber nicht als intern markiert (siehe syslet/containerhost/host.cue).

Weil die internen Dienste auf werner nur aus dem eigenen Stack erreichbar sind, stehen ihre Passwörter im Klartext in der Stack-Datei; die Begründung steht in Secrets im Repo.

Secrets

Secrets wie Passwörter und Tokens bekommen die Container als Podman Secrets; dieser Abschnitt erklärt, wie sie vom Repo dorthin kommen, was das schützt und was nicht.

Vom Repo in den Container

Im Repo liegen die Secrets verschlüsselt in creds-*.enc.yaml. syslet schickt sie beim Ausrollen so, wie sie sind, an den Host und entschlüsselt sie erst dort, mit dem Schlüssel, den es aus dem SSH-Schlüssel des Hosts ableitet. Die entschlüsselten Werte legt syslet als Podman Secrets ab: Podman verwaltet sie unter einem Namen, und ein Container bekommt nur die Secrets, die in seiner Stack-Datei stehen.

flowchart LR
    subgraph repo ["Repo"]
        creds["creds-hedgedoc-server.enc.yaml<br>(verschlüsselt)"]
    end

    subgraph host ["Host, z. B. werner"]
        syslet["syslet<br>entschlüsselt beim Ausrollen"]
        store[("Podman Secret<br>hedgedoc-server-oidcclientsecret")]
        container["Container hedgedoc-server<br>CMD_OAUTH2_CLIENT_SECRET"]
    end

    creds -->|"cue cmd apply<br>per SSH, verschlüsselt"| syslet
    syslet -->|Klartext| store
    store -->|beim Start| container

In der Stack-Datei steht dafür nur der Name des Secrets und wie es in den Container kommt, zum Beispiel in syslet/werner/stack-hedgedoc.cue:

Text Only
1
2
3
Secret: {
    "hedgedoc-server-oidcclientsecret": "type=env,target=CMD_OAUTH2_CLIENT_SECRET"
}

Wie die Namen gebildet werden, steht in syslet in diesem Repo.

Secrets kommen als Podman Secrets in die Container. Das hält Passwörter aus allem heraus, was man sich zur Fehlersuche ansieht oder weitergibt:

  • Die Werte stehen nicht in den Quadlet- und systemd-Unit-Dateien, die syslet auf den Host schreibt, sondern nur deren Namen.
  • podman inspect zeigt statt des Wertes nur *******.
  • Secrets landen weder in einem Image noch in dem, was podman commit oder podman export aus einem Container machen.

Außerdem bekommt eine Secret-Datei (type=mount) das private SELinux-Label ihres Containers. Andere Container kommen nicht daran, auch wenn sie unter derselben Benutzer-ID laufen (siehe SELinux).

Als Umgebungsvariable oder als Datei

Podman kann ein Secret auf zwei Arten in einen Container geben:

type=env
als Umgebungsvariable, im Beispiel oben CMD_OAUTH2_CLIENT_SECRET. So machen wir es bei den meisten Apps, weil sie ihre Einstellungen nur aus Umgebungsvariablen lesen können. Das ist eine Notlösung, keine Wahl.
type=mount
als Datei unter /run/secrets/<name> im Container; das ist der Standard, wenn type= fehlt. Die App bekommt dann in einer Umgebungsvariable nur den Pfad zur Datei, oft in einer Variable, die auf _FILE endet. So machen wir es überall, wo die App das kann, zum Beispiel beim Membertool (MEMBERTOOL_SMTP_PASSWORD_FILE in syslet/werner/stack-membertool.cue), beim Redirect-Dienst und beim rest-server. Mit uid=, gid= und mode= legen wir fest, welchem Benutzer im Container die Datei gehört und wer sie lesen darf.

Wo die App es kann, kommt ein Secret als Datei, nicht als Umgebungsvariable. Eine Umgebungsvariable sieht nicht nur die App selbst:

  • Jeder Prozess, den die App startet, erbt ihre Umgebungsvariablen und damit das Secret.
  • Viele Apps geben bei Fehlern oder im Debug-Modus ihre Umgebung aus, und Fehlerberichte (Crash-Reports) enthalten sie oft. Das Secret steht dann in Logs oder wird an Dritte geschickt.

Eine Datei liest die App gezielt, wenn sie den Wert braucht; sie wird weder vererbt noch nebenbei ausgegeben.

Klartext auf dem Server

Auf dem Host liegen die Secrets zwangsläufig im Klartext: Die Apps brauchen sie, um sich etwa bei der Datenbank oder bei Mailjet anzumelden. Podman speichert sie unverschlüsselt in einer Datei, die nur root lesen darf.

Den Klartext auf dem Server nehmen wir hin. Wer root auf dem Host ist, kann ohnehin alles: in jeden Container schauen, jede Datei lesen, jeden Prozess beobachten.

Was Podman Secrets nicht leisten

  • Sie liegen auf der Platte: Auch als Datei (type=mount) liegt ein Secret nicht in einem tmpfs, auch wenn der Pfad /run/secrets/ danach aussieht.
  • Mit type=env ist das Secret in der Umgebung des Prozesses lesbar (/proc/<pid>/environ) und steht in der Startkonfiguration, die Podman für den Container auf der Platte ablegt. Beides kann nur root lesen, oder der Container selbst.

Weiterlesen