Jeder Entwickler musste sich regelmäßig die Frage stellen, wie er richtig mit Passwörtern oder anderen geheimen Anmeldeinformationen (“Secrets”) umgeht. “Richtig” bedeutet: einfach zu verwalten, aber vor allem sicher. In einer Cloud wie AWS sollte man das native Berechtigungssystem (IAM) mit Rollen und Berechtigungsdelegation so weit wie möglich nutzen. Manchmal kommt man aber um die gute alte Passwortverwaltung nicht herum.
AWS bietet dafür einen eigenen Dienst an, den AWS Secrets Manager. Ein ähnliches Angebot gibt es auch von jedem anderen großen Cloud-Anbieter. Wie jeder andere Dienst in der Cloud soll er nicht manuell konfiguriert werden sondern mittels Infrastructure-as-Code (IaC). Bevor wir schauen wie das geht, müssen wir aber erstmal über das “Need-to-know”-Prinzip sprechen.
Need-to-know
Ein Grundprinzip der IT-Security heißt Need-to-know. Dies bedeutet, dass so wenig Ressourcen wie möglich Zugriff auf ein Secret erhalten sollten, um es eben bestmöglich zu schützen. Der Begriff Ressource umfasst hier sowohl Software als auch Menschen. Wenn zwei Systeme sicher über ein Passwort interagieren, warum solltest du als Mensch davon wissen? Es mag sich sicherer anfühlen, alle Passwörter zu kennen, aber es erhöht tatsächlich das Sicherheitsrisiko. Wenn du der Meinung bist, Zugriff zum Debuggen oder in einer Notfallsituation zu benötigen, ist das in Ordnung – du musst nur das Secret danach rotieren, ohne den neuen Wert zu kennen.
Werfen wir einen Blick auf ein paar Beispiele und schauen, wie wir IaC und Passwortrotation einrichten können.
Beispiel #1 – RDS – Das geht einfach
Einer der wichtigsten Anwendungsfälle für Passwörter ist die Verbindung zu Datenbanken. Glücklicherweise bietet AWS eine sehr praktische Lösung. Wenn eine Datenbank erstellt wird, kann AWS automatisch ein zugehöriges Secret im AWS Secrets Manager erstellen und verwalten. Das bedeutet:
Das Passwort wird im AWS Secret Manager hinterlegt und kann von der Anwendung von dort abgerufen werden (mithilfe von IAM). Eine automatische Rotation ist inklusive; standardmäßig alle sieben Tage. Die Anwendung muss das natürlich unterstützen – Das ist schnell programmiert: Wenn die Verbindung zu einer Datenbank mit einen “bad Password”-Fehler fehlschlägt, dann muss das Passwort erneut aus dem Secrets Manager gelesen werden – Wahrscheinlich wurde es rotiert.
Als Mensch muss man dieses Passwort nicht kennen. Man sollte sogar die AWS-Berechtigungen so einrichten, dass es nicht ausgelesen werden kann (bzw. nur von Personen mit Administratorrechten).
Verstößt dieses Vorgehen gegen das Ziel von Infrastructure-as-Code, weil das Secret von AWS erstellt wird? Ich glaube nicht. IaC bedeutet, dass die gesamte Infrastruktur über Code verwaltet werden kann, ohne dass jede Ressource explizit definiert werden muss.
Nehmen wir an, du verwendest Terraform und deklarierst deine RDS-Instanz wie folgt:
resource "aws_db_instance" "example" {
allocated_storage = 10
db_name = "mydb"
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t3.micro"
manage_master_user_password = true
username = "foo"
}
output "db_Secret_arn" {
value = aws_db_instance.example.master_user_Secret[0].Secret_arn
}
Terraform erstellt die RDS-Instanz mit einem Master-Benutzer namens “foo”. Das entsprechende Passwort wird im Secrets-Manager als Secret gespeichert. Das Passwort wird mit dem Standard-KMS-Schlüssel verschlüsselt, da wir keinen benutzerdefinierten Schlüssel definiert haben. Wir können jetzt mit dem AWS-ARN auf das Passwort verweisen, aber der Wert des Passworts ist nirgendwo sichtbar.
Das Passwort wird automatisch gelöscht, wenn die RDS-Instance gelöscht wird. Während des gesamten RDS-Lebenszyklus ist keine manuelle Interaktion mit dem Secret erforderlich – das ist IaC.
Example #2 – Apache ActiveMQ – Wir brauchen einen Workaround
Werfen wir nun einen Blick auf ActiveMQ, einen beliebten Open-Source-Message-Broker. Bei der Einrichtung muss ein Benutzernamen und ein Passwort für die weitere Konfiguration und den Zugriff definiert werden. Leider muss dies manuell eingerichtet werden, auch in der AWS-version.
Was wir NICHT machen sollten:
resource "random_password" "password" {
length = 16
special = true
override_special = "_%@"
}
resource "aws_mq_broker" "example" {
broker_name = "example"
engine_type = "ActiveMq"
engine_version = "5.17.3"
storage_type = "ebs"
host_instance_type = "mq.m5.large"
user {
username = "admin"
password = random_password.password.result
}
}
Warum? Um Unterschiede zwischen dem Soll- und dem Ist-Zustand der Infrastruktur zu erkennen, verwendet Terraform eine zwischengeschaltete “Zustandsdatei” (“State Store” oder einfach “State”). In dieser Datei wird das Passwort im Klartext wie folgt gespeichert:
"user": [
{
"console_access": false,
"groups": [],
"password": "Lig1_kWpU9rSRV_U",
"replication_user": false,
"username": "admin"
}
]
Man kann den Zugriff auf den State verschlüsseln und einschränken, dies führt jedoch zu zusätzlichem Aufwand und Risiko. Eine Alternative ist die kommerzielle Version von Terraform, bei der der State für den Endkunden verwaltet wird. Ich persönlich bin der Meinung, dass es immer besser ist, Passwörter aus dem State heraus zu halten. Falls man das anders halten möchte muss man die entsprechenden Parameter zumindest als ‘sensitive’ deklarieren.
Übrigens: Einige Leute beschweren sich, dass Secrets überhaupt im State abgespeichert werden können. Das aber muss genauso sein: Terraform muss alle relevanten Informationen speichern, um die Infrastruktur verwalten zu können, beispielsweise un Änderungen zu erkennen. Dies ist unabhängig davon, wie die Konfiguration erfolgt ist – hartcodierte Strings, Ressourcenreferenzen oder Terraform-Variablen. Alles muss persisitert werden.
Die Lösung ist die Ressourcen in Terraform zu verwalten, aber den (geheimen) Wert von Secrets außerhalb von Terraform. Dazu gibt es kommerzielle Lösungen wie Vault. Wir wollen es hier aber einfacher halten und tun stattdessen dies:
resource "aws_Secretsmanager_Secret" "mq_Secret" {
name = "mq_Secret"
}
resource "aws_mq_broker" "example" {
broker_name = "example"
engine_type = "ActiveMQ"
engine_version = "5.17.3"
storage_type = "ebs"
host_instance_type = "mq.m5.large"
user {
username = "admin"
password = "IGNORE-ME-I-AM-ALREADY-OUTDATED."
}
lifecycle {
ignore_changes = [
user,
]
}
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = "./update_Secrets.sh ${aws_Secretsmanager_Secret.mq_Secret.name} ${aws_mq_broker.example.id}"
}
}
Wir erstellen ein leeres AWS-Secret mit einem Dummy-Wert für unser MQ-Passwort. Das ignore_changes Schlüsselwort bedeutet, das Terraform Änderungen an diesem Teil der Konfiguration ignoriert. Das bedeutet, dass das ursprüngliche Dummy-Passwort für immer im State verbleibt. So weit so gut. Aber wie legen wir ein sinnvolles und geheimes Passwort fest? Dafür nutzen wir local-exec. Damit wird Terraform angewiesen ein externes Skript aufzurufen, wenn die Ressoure erstellt wird. Dieses Skript sieht so aus:
#!/bin/bash
set -eu
if [ $# -ne 2 ]; then
echo "Script to update a password both in Secretsmanager and "
echo "Active MQ managed for our admin user."
echo "Usage: $0 <aws_Secretsmanager_Secret_id> <broker_id>"
exit 1
fi
AWS_SecretSMANAGER_ID=$1
MQ_BROKER_ID=$2
echo "Generating a random password string using AWS Secrets Manager"
echo "--- Length 12, no special chars ---"
NEW_PASSWORD=$(aws Secretsmanager get-random-password --exclude-punctuation \
--password-length 12 --query 'RandomPassword' --output text)
echo "Updating Secret in AWS Secrets Manager ${AWS_SecretSMANAGER_ID}"
aws Secretsmanager put-Secret-value --Secret-id ${AWS_SecretSMANAGER_ID} \
--Secret-string "${NEW_PASSWORD}" > /dev/null
echo "Done, updating Secret in ActiveMQ ${MQ_BROKER_ID}"
aws mq update-user --broker-id ${MQ_BROKER_ID} --username "admin" \
--password ${NEW_PASSWORD}
Es macht nur drei Dinge:
- Generiert ein neues Passwort über die AWS CLI
- Speichert dieses Passwort im Secrets Manager
- Legt das Passwort als neues ActiveMQ-Passwort fest
Wenn dieses Skript erfolgreich ausgeführt wird, wird das Passwort an beiden Stellen aktualisiert, sodass unsere Anwendung, die auf ActiveMQ zugreift, es von dort abrufen kann. Man kann das Passwort auch später rotieren, wenn das Skript erneut aufgerufen wird.
Example #3 – Ein Passwort von außen benutzen – Autsch.
Manchmal muss ein Passwort aus einer externen Quelle verwendet werden, z.B. um sich mit einem 3rd-Party-System zu verbinden. Hier gibt es keine Wahlmöglichkeit. Natürlich kann man den AWS Secrets Manager nutzen und das Passwort dort speichen, z.B. mittels aws Secretsmanager put-Secret-value. Das Passwort wird dann an zwei Orten gespeichert (im CICD-System und dem Secrets-Manager), aber du kannst deine Umgebung automatisch einrichten.
Natürlich kannst du das Passwort nicht rotieren, da du es nicht besitzt.
Wenn du eine sehr gute Beziehung zu deinem Provider hast, kannst du ihn um Folgendes bitten:
- Einrichten eines AWS-Kontos
- Speichere das Geheimnis in diesem Konto
- Erlaube den Zugriff von deinem Konto aus über kontoübergreifende Berechtigungen
- Vereinbare die Passwort-Rotation
Fazit
Wenn du nur eine Sache aus dem Artikel mitnimmst, dann hoffentlich das Folgende: Der Umgang mit Passwörtern und anderen Secrets muss auf dem Need-to-know-Prinzip basieren, und wahrscheinlich müssen verschiedene Verfahren implementiert werden. Eine Einheitslösung gibt es nicht.