Vault Transform Engine: Format-erhaltende Verschluesselung fuer sensible Daten auf Nomad
In unserem vorherigen Beitrag ueber HashiCorp Nomad und Vault: Dynamic Secrets haben wir den gesamten Lebenszyklus des Secrets Managements fuer eine Python

In unserem vorherigen Beitrag ueber HashiCorp Nomad und Vault: Dynamic Secrets haben wir den gesamten Lebenszyklus des Secrets Managements fuer eine Python Flask-Anwendung auf Nomad beschrieben — von hartcodierten Zugangsdaten bis hin zur Vault Transit-Verschluesselung. Transit ist leistungsstark: Eine Kreditkartennummer wird zu vault:v1:abc123... — unlesbarem Ciphertext, der nicht missbraucht werden kann.
Aber genau diese Staerke ist manchmal ein Problem.
Stellen Sie sich ein Legacy-System vor, das das Format einer Sozialversicherungsnummer vor dem Speichern validiert, oder ein Data Warehouse, das erwartet, dass Kreditkartennummern auch nach der Verschluesselung noch wie Kreditkartennummern aussehen. Den Wert durch undurchsichtigen Ciphertext zu ersetzen, bricht diese Systeme. Sie benoetigen format-erhaltende Verschluesselung (FPE) — und genau das bietet die Vault Transform Secret Engine.
| Transit | Transform (FPE) | Transform (Masking) | |
|---|---|---|---|
| Ausgabe sieht wie Eingabe aus | ✗ | ✓ | ✗ |
| Umkehrbar | ✓ | ✓ | ✗ |
| Anwendungsfall | Allgemeine Verschluesselung | Regulierte Daten (SSN, PAN) mit Formatvorgaben | Einweg-Verschleierung (Logs, Anzeige) |
| Vault Engine | transit | transform | transform |
In unserer Demo-Anwendung nutzen wir alle drei:
birth_date, address, salary (allgemeine Felder, Ciphertext ist akzeptabel)ssn (Sozialversicherungsnummer, muss im Format NNN-NN-NNNN bleiben)ccn (Kreditkartennummer, wird als XXXX-XXXX-XXXX-1234 in Anzeigeansichten dargestellt)Die Vault Transform Engine ist von der Transit Engine getrennt. Aktivieren Sie sie an einem dedizierten Mount-Punkt.
Hinweis: Transform ist ein Vault-Enterprise-Feature (auch auf HCP Vault Dedicated verfuegbar).
1# Transform Secret Engine aktivieren
2vault secrets enable -path=dynamic-app/transform transform
3
4# Zweiten Mount fuer Masking aktivieren (haelt Policies sauber getrennt)
5vault secrets enable -path=dynamic-app/transform-masking transform
FPE fuer SSNs verwendet Vaults eingebautes Template builtin/socialsecuritynumber, das das NNN-NN-NNNN-Muster erzwingt. Der verschluesselte Wert ist ebenfalls eine gueltig aussehende SSN — nachgelagerte Format-Validatoren werden ohne Aenderungen bestanden.
1# FPE-Transformation fuer SSN erstellen
2vault write dynamic-app/transform/transformation/ssn \
3 type=fpe \
4 template=builtin/socialsecuritynumber \
5 tweak_source=internal \
6 allowed_roles=ssn
7
8# Rolle erstellen
9vault write dynamic-app/transform/role/ssn \
10 transformations=ssn
Kreditkartennummern werden maskiert statt FPE-verschluesselt. Das Ergebnis ist nicht umkehrbar (XXXX-XXXX-XXXX-1234) und eignet sich fuer die Anzeige in Logs, Audit-Trails und Read-only-Ansichten.
1# Masking-Transformation fuer CCN erstellen
2vault write dynamic-app/transform-masking/transformation/ccn \
3 type=masking \
4 masking_character=X \
5 template=builtin/creditcardnumber \
6 allowed_roles=ccn
7
8vault write dynamic-app/transform-masking/role/ccn \
9 transformations=ccn
Erweitern Sie die bestehende Policy nomad-dynamic-app um die Berechtigungen fuer Encode und Decode auf den neuen Pfaden:
1# Bestehende Transit-Berechtigungen (gekuerzt)
2path "dynamic-app/transit/encrypt/app" {
3 capabilities = ["create", "update"]
4}
5path "dynamic-app/transit/decrypt/app" {
6 capabilities = ["create", "update"]
7}
8
9# Neue Transform-Berechtigungen
10path "dynamic-app/transform/encode/ssn" {
11 capabilities = ["create", "update"]
12}
13path "dynamic-app/transform/decode/ssn" {
14 capabilities = ["create", "update"]
15}
16path "dynamic-app/transform-masking/encode/ccn" {
17 capabilities = ["create", "update"]
18}
Fuer CCN wird keine decode-Berechtigung benoetigt, da Masking irreversibel ist.
db_client_transform.pyDer DbClient in db_client_transform.py erweitert den Transit-Client. Er erbt das gesamte Transit-Verhalten fuer birth_date, address und salary und fuegt die SSN- und CCN-Behandlung hinzu.
Das Vault Python SDK (hvac) bietet Transform-API-Support. In dieser Implementierung verwenden wir fuer Transform-Aufrufe dennoch hvacs zugrunde liegenden HTTP-Adapter, damit die Request-Verarbeitung explizit bleibt und ueber alle Sprachvarianten konsistent ist. Dies integriert sich sauber mit hvacs Session-Management und SSL-Konfiguration.
1def init_transform(
2 self,
3 transform_path,
4 transform_masking_path,
5 ssn_role,
6 ccn_role,
7):
8 self.transform_mount_point = transform_path
9 self.transform_masking_mount_point = transform_masking_path
10 self.ssn_role = ssn_role
11 self.ccn_role = ccn_role
12 logger.debug(f"Initialized transform: {self.vault_client}")
transform_mount_point und transform_masking_mount_point werden aus config.ini gelesen — die Anwendung ist vollstaendig konfigurationsgesteuert ohne hartcodierte Pfade.
1def encode_ssn(self, value):
2 try:
3 url = (
4 self.vault_client.url
5 + "/v1/"
6 + self.transform_mount_point
7 + "/encode/"
8 + self.ssn_role
9 )
10 headers = {
11 "X-Vault-Token": self.vault_client.token,
12 "X-Vault-Namespace": super().get_namespace(),
13 "Content-Type": "application/json",
14 "cache-control": "no-cache",
15 }
16 # hvac Adapter fuer HTTP-Aufrufe verwenden
17 response = self.vault_client.adapter.post(
18 url=url,
19 json={"value": value, "transformation": self.ssn_role},
20 headers=headers,
21 timeout=300,
22 )
23 logger.debug(f"Response: {response.text}")
24 return response.json()["data"]["encoded_value"]
25 except Exception as e:
26 logger.error(f"There was an error encrypting the data: {e}")
27 return ""
Der Eingabewert 123-45-6789 wird zu etwas wie 987-65-4321 — numerisch verschluesselt, strukturell identisch. Beachten Sie, dass wir self.vault_client.adapter.post() mit json={} verwenden — dies ist sauberer als rohes String-Building von Payloads.
1def decode_ssn(self, value):
2 logger.debug(f"Decoding {value}")
3 try:
4 url = (
5 self.vault_client.url
6 + "/v1/"
7 + self.transform_mount_point
8 + "/decode/"
9 + self.ssn_role
10 )
11 headers = {
12 "X-Vault-Token": self.vault_client.token,
13 "X-Vault-Namespace": self.get_namespace(),
14 "Content-Type": "application/json",
15 "cache-control": "no-cache",
16 }
17 response = self.vault_client.adapter.post(
18 url=url,
19 json={"value": value, "transformation": self.ssn_role},
20 headers=headers,
21 timeout=300,
22 )
23 logger.debug(f"Response: {response.text}")
24 return response.json()["data"]["decoded_value"]
25 except Exception as e:
26 logger.error(f"There was an error decoding the data: {e}")
27 return None
Das Dekodieren ist die exakte Umkehrung. Nur Anwendungen mit dem richtigen Vault-Token und der richtigen Policy koennen den Originalwert abrufen.
Die Methode process_customer wendet selektiv die Entschluesselung je nach Feldtyp an — Transit fuer birth_date, address und salary; Transform-Decode fuer ssn; CCN wird nie dekodiert (Masking ist irreversibel):
1def process_customer(self, row, raw=None):
2 r = { ... }
3 if self.vault_client is not None and not raw:
4 r["birth_date"] = self.decrypt(r["birth_date"]) # Transit
5 r["ssn"] = self.decode_ssn(r["ssn"]) # Transform FPE
6 r["address"] = self.decrypt(r["address"]) # Transit
7 r["salary"] = self.decrypt(r["salary"]) # Transit
8 # ccn bleibt maskiert — kein Decode
9 return r
Die Nomad Job-Datei fuegt den Transform-Konfigurationsabschnitt zur gerenderten config.ini hinzu. Am vault-Stanza sind keine Aenderungen erforderlich — die bestehende Policy nomad-dynamic-app deckt die oben hinzugefuegten neuen Pfade bereits ab.
1task "dynamic-app" {
2 driver = "docker"
3
4 config {
5 image = "quay.io/infralovers/nomad-vault-mysql"
6 volumes = ["local/config.ini:/usr/src/app/config/config.ini"]
7 ports = ["web"]
8 }
9
10 template {
11 destination = "local/config.ini"
12 data = <<EOF
13[DEFAULT]
14LogLevel = DEBUG
15Port = 8080
16
17[DATABASE]
18{{ range service "mysql-server" }}
19Address = {{ .Address }}
20Port = {{ .Port }}
21{{ end }}
22{{ with secret "dynamic-app/kv/database" }}
23Database = {{ .Data.data.database }}
24{{ end }}
25{{ with secret "dynamic-app/db/creds/app" }}
26User = {{ .Data.username }}
27Password = {{ .Data.password }}
28{{ end }}
29
30[VAULT]
31Enabled = True
32InjectToken = True
33Namespace =
34Address = {{ env "VAULT_ADDR" }}
35KeyPath = dynamic-app/transit
36KeyName = app
37
38[VAULT_TRANSFORM]
39Enabled = True
40TransformPath = dynamic-app/transform
41TransformMaskingPath = dynamic-app/transform-masking
42SsnRole = ssn
43CcnRole = ccn
44EOF
45 }
46}
Nach dem Einfuegen eines Kundendatensatzes zeigt die folgende Tabelle, wie die rohen Datenbankzeilen im Vergleich zur Anwendungsansicht aussehen:
| Feld | Roher DB-Wert | Dargestellt (App-Ansicht) |
|---|---|---|
birth_date | vault:v1:AbC123... | 1985-04-12 |
ssn | 987-65-4321 | 123-45-6789 |
ccn | XXXX-XXXX-XXXX-1234 | XXXX-XXXX-XXXX-1234 |
address | vault:v1:XyZ456... | Hauptstrasse 42 |
salary | vault:v1:PqR789... | 85000 |
Die SSN-Spalte sieht in der Datenbank wie eine gueltige Sozialversicherungsnummer aus — sie besteht Format-Validatoren, kann indiziert werden und ist nur ueber Vault durch eine autorisierte Anwendung wiederherstellbar.
Die Vault Transform Engine fuellt eine wichtige Luecke im Secrets-Management-Toolkit:
Alle drei Methoden arbeiten mit dem gleichen Nomad + Consul Template-Muster und erfordern nur eine kleine Erweiterung der config.ini und eine aktualisierte Vault Policy. Die Aenderung am Anwendungscode ist minimal und auf db_client_transform.py beschraenkt.
Der vollstaendige Quellcode ist unter github.com/infralovers/nomad-vault-mysql verfuegbar.
Sie interessieren sich für unsere Trainings oder haben einfach eine Frage, die beantwortet werden muss? Sie können uns jederzeit kontaktieren! Wir werden unser Bestes tun, um alle Ihre Fragen zu beantworten.
Hier kontaktieren