In een vorige blogpost hebben we de voordelen besproken van vertrouwelijke 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
De Kubernetescapsules, die gebruikt worden als abstractie voor de vertrouwelijke gecontaineriseerde workloads, brengen een aantal uitdagingen met zich mee. 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 capsule 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 user 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-capsule 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 wordt 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 sleutelbemiddelingsservice, die kan worden aangesloten op een transactionele black box, stelt de container in staat om dynamisch de middelen te verkrijgen die nodig zijn voor zijn werking. 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 gebouwd 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 gegeven.
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 vertrouwelijke containers 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 Figure 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 Figure 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 clients, 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 klanten nog meer garanties bieden.
Dergelijke systemen zijn eenvoudiger te gebruiken dan geavanceerde cryptografische methoden en zouden ons ook in staat kunnen stellen om meer algemene problemen op te lossen dan cryptografie of problemen die we tot nu toe niet konden oplossen.