In een vorige blogpost hebben we de voordelen besproken van confidential containers en hun architectuur in het CoCo-project. In deze blogpost gaan we dieper in op het onderwerp door bepaalde aspecten van CoCo in detail te beschrijven en onze installatie op onze eigen hardware toe te lichten.
Containercertificering
Het gebruik van Kubernetes-pods als abstractielaag voor vertrouwelijke container-workloads introduceert diverse uitdagingen. Door hun dynamische karakter – het maken, verwijderen, updaten van de containers – en de invloed van de Kubernetesomgeving (omgevingsvariabelen, toelatingscontrollers, enz.) valt het moeilijk te garanderen dat enkel de door de gebruiker bedoelde code wordt uitgevoerd. Zo kan het injecteren van kwaadaardige variabelen of het wijzigen van de specificatie van een pod voordat deze wordt gestart, de vertrouwelijkheid in gevaar brengen.
Het CoCo-project stelt een elegante oplossing voor, namelijk het gebruik van een engine voor beveiligingsbeleid, geïntegreerd in de containerruntime-omgeving binnen de trusted execution environment (TEE), die de door de gebruiker gedefinieerde regels toepast. Deze engine kan bijvoorbeeld alleen bepaalde images of commando’s toestaan en problematische verzoeken (zoals het uitvoeren van ongeoorloofde processen) afwijzen. Figuur 1 toont een voorbeeld van zo’n beleid.
package agent_policy
# Seules certaines images de conteneurs peuvent être exécutées
default CreateContainerRequest := false
CreateContainerRequest if {
every storage in input.storages {
some allowed_image in policy_data.allowed_images
storage.source == allowed_image
}
}
# Seules certaines commandes peuvent être exécutées
# via ‘kubectl exec’ dans les images de conteneurs
default ExecProcessRequest := false
ExecProcessRequest if {
input_command = concat(" ", input.process.Args)
some allowed_command in policy_data.allowed_commands
input_command == allowed_command
}
policy_data := {
"allowed_commands": [
"ls",
"cat",
],
"allowed_images": [
"pause",
"my-registry.be/,my-app@sha256:5ed86f469bbc40026a0235dd92e2b0b0c7ce54e3b254132e271a9b9e85d5f220
",
],
}Figuur 1 – Voorbeeld van een beperkend beveiligingsbeleid voor images die kunnen worden uitgevoerd en de commando’s die in de image kunnen worden aangeroepen. Dit beleid wordt toegepast door een agent die in de vertrouwelijke VM zit.
Vier componenten van de vertrouwelijke virtuele guestmachine worden altijd gecontroleerd om te bepalen of ze nog goed werken: de firmware (bijvoorbeeld OVMF), de kernel van het besturingssysteem, de kernel commandoregel en het rootbestandssysteem (Figuur 2). Een vertrouwelijke externe entiteit, vaak Trustee genoemd, zorgt ervoor dat de vertrouwensketen versterkt wordt.
Vertrouwelijke containers hebben echter meestal initialisatiedata nodig die niet direct in de image van de virtuele machine of de toepassingscontainer kunnen worden opgenomen, zoals certificaten, adressen van certificeringsdiensten of toe te passen beveiligingsbeleidsregels. Deze data zijn weliswaar niet geheim, maar moeten wel worden beschermd tegen wijzigingen.
Deze initialisatiedata, ook wel init-data genoemd, kunnen worden opgegeven in de vorm van een woordenboek (bijv. JSON-bestanden, TOML, YAML), gecodeerd in base64 en doorgegeven aan de Kubernetes-pod via een Kubernetes annotation (Figuur 3). Om de integriteit ervan te garanderen, wordt hun cryptografische hashwaarde door de certificeringsagent (die in de vertrouwelijke virtuele machine draait) als data voor de berekening van de certificering verstrekt (dit kan worden gedaan met behulp van het veld “HostData” van SEV-SNP). Het is dan mogelijk om de initialisatiedata die naar de hostmachine zijn gestuurd voor het starten van de container te vergelijken met de hashwaarde die op het moment van de certificering is ontvangen, zodat elke wijziging tijdens de certificering op afstand kan worden gedetecteerd.
version = "0.1.0"
algorithm = "sha256"
[data]
# Configuration de l’agent d’attestation
"aa.toml" = '''
[token_configs]
[token_configs.kbs]
url = "${KBS_ADDRESS}"
'''
# Configuration du gestionnaire de données secrètes
"cdh.toml" = '''
[kbc]
name = "cc_kbc"
url = "${KBS_ADDRESS}"
[image]
authenticated_registry_credentials_uri = "kbs:///${REGISTRY_AUTH_KBS_PATH}"
image_security_policy_uri = "${SECURITY_POLICY_KBS_URI}"
'''
# Politique de sécurité restreignant l’environnement du conteneur
"policy.rego"= '''
[Voir Figure 1 ci-dessus]
'''Figuur 3 – Voorbeeld van initialisatiedata die (in gecodeerde vorm) via een Kubernetes-annotatie aan de CoCo-guestagent in de vertrouwelijke virtuele machine worden geleverd.
Sleutelbeheer
Een externe sleutelbemiddelingsdienst (key broker service), die kan worden gekoppeld aan een transactionele ‘black box’, stelt de container in staat om dynamisch de resources op te halen die nodig zijn voor de werking ervan. Indien de client nog niet in het bezit is van een eerder verkregen authenticatietoken van de sleutelbemiddelingsdienst, moet hij zich eerst authenticeren, waarna de sleutelbemiddelingsdienst hem een challenge stuurt die hij moet beantwoorden (Figuur 4).
De client genereert een paar cryptografische sleutels en vraagt de processor om een certificaat te verstrekken met daarin de hashwaarde van zijn openbare sleutel en een unieke willekeurige waarde die door de dienst in zijn challenge is verzonden. Het certificaat dat de openbare sleutel van de client, de unieke willekeurige waarde die door de service is gestuurd en de meting van de vertrouwelijke VM die de client bevat aan elkaar koppelt, wordt door de processor ondertekend. De service gebruikt een certificeringsagent die het certificaat controleert door de handtekening te verifiëren en de meting te vergelijken met een referentiewaarde.
Installatie en testen
Om de CoCo-omgeving te testen, hebben we gekozen voor een EPYC 9335-microprocessor van AMD. Deze maakt gebruik van SEV-SNP-technologie voor versleuteling en bescherming van de integriteit van het RAM-geheugen. We hebben een machine geassembleerd met een moederbord dat deze microprocessor ondersteunt (Supermicro MBD-H13SSL-NT-O) en 128 GB RAM-geheugen. Vervolgens moesten we het BIOS configureren om ervoor te zorgen dat de gewenste beveiligingsfuncties van de microprocessor goed waren geactiveerd. We hebben ook gekozen voor de Ubuntu 24.04.3 LTS-distributie van het Linux-besturingssysteem. Voordat we de beveiligingsfuncties van de processor konden testen, moesten we ten slotte de kernel van het besturingssysteem opnieuw compileren. Dit is eigenlijk vrij simpel dankzij de scripts die AMD heeft meegegeven.
Eenmaal het systeem is ingesteld, kun je het Docker-platform installeren (om containerimages te maken), de containeruitvoeringsinterface containerd (inbegrepen in de Docker-distributie) en het Kubernetes-beheersysteem. Het instellen van deze tools is best lastig en afhankelijk van de versie. Er zijn verschillende scripts beschikbaar om deze installatie te vergemakkelijken.
Nadat het systeem was geïnstalleerd, konden we een bestaande toepassing in vertrouwelijke containers zetten: je hoeft alleen maar de naam van de runtimeklasse die Kubernetes gebruikt (runtimeClassName) in het YAML-configuratiebestand van Kubernetes te veranderen in een van de CoCo-klassen (bijvoorbeeld kata-qemu-snp). Natuurlijk is deze simpele wijziging niet genoeg om te profiteren van de beveiligingsfuncties van CoCo. Je moet de productiecyclus aanpassen om de volgende stappen toe te voegen:
- Versleuteling van de containerimage
- Ondertekening van de containerimage
- Beschikbaar stellen van versleutelings- en ondertekeningssleutels
Zodra de containerimage op de gebruikelijke manier is gemaakt, bijvoorbeeld met docker build, kan deze worden versleuteld met de tool skopeo, die verschillende algoritmen ondersteunt: JWE (RFC7516), PGP (RFC4880) en PKCS7 (RFC2315). Deze versleutelde image kan vervolgens worden ondertekend met de tool cosign en ten slotte worden geüpload naar een imageregister.
Bij het opstarten van de container moeten de CoCo-componenten in de vertrouwelijke virtuele machine de handtekening kunnen verifiëren en de image kunnen ontsleutelen. Hiervoor moeten de benodigde sleutels beschikbaar worden gesteld. Hier komt het sleutelbemiddelingssysteem om de hoek kijken. Zoals we eerder hebben gezien, voert dit systeem een certificeringsprotocol uit voordat het de sleutels verstrekt.
De implementatie van confidential ccontainers is transparant voor de gebruiker van Kubernetes. Zodra het gebruikelijke commando kubectl apply wordt aangeroepen, wordt een lichte Kata-virtuele machine aangemaakt. Deze moet bij de sleutelbemiddelaar de toegangssleutel tot het imageregister (als dit niet openbaar is), het toe te passen beveiligingsbeleid, de sleutel voor handtekeningverificatie en de sleutel voor het ontsleutelen van de image ophalen. Deze informatie wordt pas verstrekt nadat de virtuele machine is geverifieerd (zie hierboven). De agents in de virtuele machine kunnen dan het beveiligingsbeleid toepassen, de image downloaden, de handtekening controleren en deze decoderen voordat de toepassingscontainer in de virtuele machine wordt gestart.
Wat betreft de communicatie van de gecontaineriseerde toepassing met externe diensten, moeten wederzijds erkende versleutelingssleutels worden ingesteld. Een eerste mogelijkheid is dat de vertrouwde container bij het opstarten een cryptografisch sleutelpaar aanmaakt en de cryptografische hashwaarde van deze openbare sleutel bij de certificering verstrekt. Dit wordt gebruikt binnen het authenticatieprotocol dat in Figuur 4 wordt beschreven. Een andere optie is om de openbare sleutel van een certificeringsinstantie in de versleutelde en vervolgens ondertekende image te verstrekken. De container kan dan de certificaten checken die deze autoriteit heeft ondertekend en de encryptiesleutels aanvaarden. Een derde optie bestaat erin om te steunen op de sleutelbemiddelingsdienst: hiermee kan de container op een veilige manier geheimen ophalen. Afhankelijk van de gekozen optie moet de code van de toepassing al dan niet worden aangepast.
Bescherming tegen een beheerder
Wat kan een beheerder van de hostmachine doen? In principe niet veel, behalve de container opstarten.
Het certificeringsmechanisme zorgt er namelijk voor dat hij niets kan vervangen of simuleren wat betreft de onderdelen van de virtuele machine die wordt gebruikt om de containers te starten. Door de versleuteling van het geheugen dat aan de virtuele machine is toegewezen, heeft hij geen toegang tot de data die in de virtuele machine en de container worden verwerkt. Door de versleuteling en ondertekening van de containerimage kan hij geen andere container vervangen of de aard van de container achterhalen. In de veronderstelling dat de toepassing geconfigureerd is om versleuteld te communiceren met externe diensten waarmee ze moet interageren, kan de beheerder ook geen toegang krijgen tot gevoelige data door het netwerkverkeer te observeren, tenzij hij ook bevoorrechte toegang heeft tot het systeem voor het aanmaken van sleutels. Ten slotte kan hij de container ook niet ondervragen via het commando kubectl exec, omdat het kan worden beperkt door een beveiligingsbeleid (zie Figuur 1).
De beheerder kan daarentegen de toepassingslogboeken lezen die door Kubernetes op de host zijn opgeslagen. Daarom is het belangrijk dat de workload provider ervoor zorgt dat zijn code geen gevoelige informatie onthult in de gelogde berichten van de toepassing.
Tot slot, zoals we in de vorige blogpost al stelden, zijn vertrouwde uitvoeringsomgevingen niet perfect en houdt hun beveiligingsmodel meestal geen rekening met fysieke aanvallen. In een omgeving zoals de G-Cloud biedt de toevoeging ervan tal van mogelijkheden. In een omgeving waar echter noch SMALS, noch haar klanten, noch zelfs de Belgische Staat enige technische of juridische controle hebben over de infrastructuur, zijn er aanzienlijke risico’s die serieus moeten worden geëvalueerd.
Conclusie
In deze blogpost en de vorige hebben we de echte voordelen belicht op het gebied van beveiliging die microprocessors kunnen bieden om “vertrouwde uitvoeringsomgevingen” binnen een IT-infrastructuur te creëren. Vooral het “on-premise” gebruik ervan maakt het mogelijk om gecontaineriseerde toepassingen beter te beschermen tegen kwaadwillige beheerders of indringers en zo onze leden nog meer garanties te bieden.
Omdat ze eenvoudiger in gebruik zijn dan geavanceerde cryptografische methoden, kunnen dergelijke systemen ons ook helpen om meer generieke problemen op te lossen dan met cryptografie alleen, of problemen die we tot nu toe simpelweg niet konden oplossen.