Kai Ole Hartwig
7 Min. Lesezeit
Niedrig

Das ganze Netz auf Pod-Zertifikate

Für alle, die Dienste in Kubernetes über mTLS verbinden und dafür cert-manager nutzen. Im ersten Beitrag stand noch, dass die Zertifikate innerhalb der Mandanten bei cert-manager bleiben. Einen Tag später habe ich das umgeworfen. Hier steht, warum, wie es jetzt aussieht und was sich erst beim Messen gezeigt hat.

01 — Warum die Entscheidung kippte

Jeder Mandant hatte eine eigene CA, und ihr Schlüssel lag als Secret im Namensraum des Mandanten. Damit war die Isolation eine Eigenschaft der Vertrauensbasis: Ein Zertifikat von Mandant A prüft bei Mandant B schlicht nicht. Das wollte ich nicht aufgeben, und eine gemeinsame CA hätte genau das verlangt.

Dann habe ich eine Zeile geprüft, die ich bis dahin nur angenommen hatte:

 

kubectl auth can-i list secrets --all-namespaces \
  --as system:serviceaccount:kube-system:traefik
yes

 

Der Edge-Proxy, der jede Anfrage aus dem Internet parst, durfte also jedes Secret im Cluster lesen. So ist das mitgelieferte Chart voreingestellt. Damit lag jeder CA-Schlüssel jedes Mandanten in Reichweite des exponiertesten Prozesses. Ein Signer je Mandant löst beide Probleme zugleich: Die Isolation bleibt an der Vertrauensbasis, und kein CA-Schlüssel liegt mehr in einem Secret.

02 — Ein Signer je Mandant

Der Signer aus dem ersten Beitrag läuft jetzt einmal je Mandant. Jede Instanz hat einen eigenen Signer-Namen (koh.ole-hartwig.eu/tenant-<namespace>), einen eigenen Schlüssel in AWS KMS und eine eigene IAM-Rolle, die genau diesen einen Schlüssel benutzen darf. Wer eine Signer-Instanz übernimmt, kann deshalb für einen Mandanten unterschreiben, solange sie läuft. Mitnehmen kann er den Schlüssel nicht.

Die CA jedes Mandanten trägt außerdem Name Constraints. Erlaubt sind nur die Dienstnamen des eigenen Namensraums und die Kurznamen, die die Dienste tatsächlich wählen: db, cache, app und caddy-proxy. Beim Start prüft der Signer jede Freigabe gegen diese Constraints, und eine Freigabe, die die CA nie einlösen könnte, verhindert den Start.

Weil Freigaben je ServiceAccount gelten, hat jede Komponente ihren eigenen. Ein Zertifikat für default hätte sonst jedem Pod im Namensraum dieselbe Identität gegeben.

03 — Umstellen in zwei Phasen

Erst kam das Vertrauen, dann die Identität. In Phase eins bekam jeder Pod eine Datei mit beiden CAs, der neuen aus KMS und der alten von cert-manager. Die alte habe ich dafür als zweites ClusterTrustBundle unter demselben Signer-Namen veröffentlicht. So konnte danach jede Verbindung einzeln wechseln und bei Bedarf auch einzeln zurück: Cache, Datenbank, App und Mandanten-Proxy.

Wie lange ein Zertifikat gilt, richtet sich nach dem Leser, nicht nach dem Aussteller. Wo der Server ein erneuertes Zertifikat neu lädt, sind es 24 Stunden. Wo nichts im Workload die Datei erneut liest, sind es 90 Tage. Ein Pod-Zertifikat ist eben nur so frisch wie der Prozess, der es liest.

Ein Detail am Rand: Der Proxy hat die App bisher unter ihrem öffentlichen Hostnamen angefragt, und den darf die Mandanten-CA nicht bestätigen. Deshalb fragt er jetzt unter dem internen Namen app, und die App liefert ihr Zertifikat für genau diesen Namen aus.

04 — Was erst die Messung zeigte

Der kubelet ignoriert defaultMode. Die Datenbank eines internen Werkzeugs startete mit dem Pod-Zertifikat nicht mehr und meldete private key file has group or world access. Also habe ich zwei Test-Pods gestartet, einen mit defaultMode 0400 und einen mit 0440. Beide zeigten dasselbe:

 

-rw-r-----  1 999  999  ca.crt
-rw-r-----  1 999  999  kv.crt
-rw-r-----  1 999  999  kv.key

 

Die Dateien haben immer 0640 und gehören dem runAsUser des Containers, ohne einen solchen root. PostgreSQL akzeptiert 0640 aber nur, wenn root der Eigentümer ist. Die Lösung war deshalb keine andere Dateiberechtigung, sondern das Weglassen der festen uid, denn das Image bringt seinen Benutzer selbst mit.

Zurückschalten ließ einen ServiceAccount hängen. Beim Rückbau entfernte Argo CD serviceAccountName aus dem StatefulSet. Der API-Server hatte den Namen jedoch auch in das veraltete Feld serviceAccount gespiegelt, und das gehört keinem Field Manager. Es blieb stehen und setzte den Namen gleich wieder. Den ServiceAccount selbst hatte Argo da schon entfernt, sodass sich der Pod nicht mehr anlegen ließ. Seitdem folgen ServiceAccount-Namen keinem Schalter mehr.

HTTP-01 im falschen Namensraum. Seit der Korrektur aus Abschnitt 01 liest der Edge-Proxy nur noch die Namensräume, die er bedient. cert-managers Solver für ein öffentliches Zertifikat lag aber in einem Namensraum ohne Webverkehr, dafür mit Zugangsdaten, und die Challenge bekam 404. Den Namensraum freizugeben hätte dem Proxy wieder Secrets gezeigt. Deshalb läuft dieses Zertifikat jetzt über DNS-01, mit einer Route-53-Rolle, die nur TXT-Einträge unter genau diesem Challenge-Namen schreiben darf.

Der Selbsttest lief über den falschen DNS. Der TXT-Eintrag stand längst bei beiden autoritativen Servern, trotzdem meldete cert-manager alle zehn Sekunden „not yet propagated“. Sein Selbsttest fragte nämlich das Cluster-DNS. Dort beantwortet ein Hairpin-Eintrag die ganze Zone, weshalb die SOA-Abfrage leer zurückkam. Seitdem prüft cert-manager nur noch über öffentliche Resolver.

Argo CD wartete auf sich selbst. Die übergeordnete Anwendung wartete darauf, dass alle Mandanten gesund sind. Ein Mandant wartete auf sein Zertifikat, das Zertifikat auf einen Issuer, und diesen Issuer legt nur die übergeordnete Anwendung an. So bewegte sich stundenlang nichts, bis ich den Issuer direkt angewendet habe, identisch zu Git.

05 — Was es gekostet hat

Es gab zwei kleine Ausfälle. Beim ersten Durchgang starteten Cache und Datenbank aller Mandanten gleichzeitig neu. Weil beide mit einer Replik laufen, dauert jeder Neustart 45 bis 60 Sekunden, und die Seiten-Proben schlugen bei allen Mandanten bis zu drei Minuten lang fehl. Danach bin ich Mandant für Mandant vorgegangen.

Der zweite Ausfall traf die Datenbank des internen Werkzeugs aus Abschnitt 04, für rund 45 Minuten. Der Test vor dem Merge hatte nur das gerenderte Manifest geprüft, die Dateirechte entstehen aber erst zur Laufzeit. Heute schlägt ein Gate fehl, wenn ein PostgreSQL-Container seinen Schlüssel aus einem Pod-Zertifikat liest und dabei eine feste uid setzt. Eine absichtlich eingebaute Mutation zeigt, dass es rot werden kann.

06 — Was noch bei cert-manager liegt

Öffentliche Zertifikate bleiben bei Let's Encrypt über cert-manager. Beim Client-Zertifikat, mit dem sich der Edge-Proxy gegenüber jedem Mandanten ausweist, hat sich dagegen seit dem ersten Entwurf etwas bewegt. Traefik lädt eine Zertifikatsdatei nur neu, wenn sich seine Konfiguration ändert. Deshalb schreibt ein Sidecar bei jeder Erneuerung die Konfiguration neu, mit dem Hash des Zertifikats im Dateinamen. Seit dem 30. September läuft das im Cluster: Traefik trägt ein Pod-Zertifikat je Mandant und sechs Transports aus dieser Datei. Die Mandanten schalte ich jetzt einzeln darauf um, mit einem Pilot-Mandanten zuerst.

Auch das Frontend des internen Werkzeugs aus Abschnitt 04 ist auf dem Weg. Der Edge-Proxy spricht es inzwischen unter dem internen Namen httpd an, als Nächstes bekommt es sein eigenes Pod-Zertifikat.

Die Signaturschlüssel für Content Credentials bleiben vorerst, bis sie nach KMS ziehen. Sie sind eine veröffentlichte Identität und keine Transportverbindung.

Häufige Fragen

Warum nicht eine gemeinsame CA für alles?+

Dann prüft jedes Zertifikat überall, und jeder Server müsste die Isolation selbst herstellen, indem er die Identität der Gegenseite prüft. Jede vergessene Einstellung wäre dann offen. Mit einer CA je Mandant bleibt die Isolation dagegen eine Eigenschaft der Vertrauensbasis.

Was passiert, wenn ein Signer ausfällt?+

Laufende Pods behalten ihr Zertifikat bis zum Ablauf. Ein neuer Pod startet aber erst, wenn sein Signer geantwortet hat, und das gilt auch für die Datenbank. Deshalb laufen die Signer verteilt über mehrere Knoten, und ein Request, der länger als fünf Minuten offen ist, löst einen Alarm aus.

Lohnt sich das ohne mehrere Mandanten?+

Der größte Gewinn ist, dass kein privater Schlüssel mehr in einem Secret liegt, und den hast du auch mit einem einzigen Namensraum. Ein Signer je Mandant lohnt sich erst, wenn sich die Mandanten gegenseitig nicht vertrauen sollen.

Fazit

Ein Signer je Mandant erhält die Isolation und nimmt die CA-Schlüssel aus der Reichweite des Clusters. Der Umbau selbst ging schnell. Teuer wurden die Stellen, an denen ich etwas angenommen statt gemessen hatte: wem eine Datei gehört, was ein Feld beim Entfernen zurücklässt und welcher DNS eine Frage beantwortet.

mTLS in deinem Cluster? Ich prüfe, was es wirklich schützt.

Eine Durchsicht deiner Zertifikate: wer welche Schlüssel lesen kann, wo die Isolation zwischen Mandanten tatsächlich liegt und was deine Dienste beim Erneuern tun.

Termin buchen →

Über den Autor

Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freiberuflicher DevSecOps-Berater · OnlyOle Consulting

Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.