AWS Secrets Manager und automatische Rotation für Passwörter
October 10, 2020Der AWS Secrets Manager ist ideal zum Speichern und Verwalten von Zugangsdaten, API Keys oder anderen sensiblen Informationen. Neben der unkomplizierten API zum Lesen und Schreiben, bietet der Secrets Manager eine automatische Rotation von gespeicherten Daten. Mit einer AWS Lambda Funktion lässt sich so nach einem definierten Zeitraum automatisch ein neues Passwort genieren.
Architektur
Ähnlich wie bei der Cross-Account Freigabe von Daten mit AWS Secrets Manager und KMS beinhaltet die Architektur einen KMS Schlüssel mit den notwendigen Berechtigungen. Mit ihm werden die gespeicherten Daten im Secrets Manager ver- und entschlüsselt. Die AWS Lambda Funktion benötigt eine entsprechenden Berechtigungen um das neue Passwort speichern zu können:
Key:
Type: AWS::KMS::Key
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
Properties:
KeyPolicy:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- kms:*
Resource: "*"
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
- Sid: "Allow use of the key"
Effect: Allow
Principal:
AWS: !GetAtt LambdaExecutionRole.Arn
Action:
- "kms:Encrypt"
- "kms:Decrypt"
- "kms:ReEncrypt*"
- "kms:GenerateDataKey*"
- "kms:DescribeKey"
Resource: "*"
Mit Verweis auf den erstellen KMS Schlüssel lässt sich nun ein neuer Datensatz im AWS Secrets Manager anlegen. Damit die automatische Rotation aktiviert ist, wird zusätzlich eine RotationSchedule
angelegt und mit einem Intervall in Tagen, sowie der Referenz auf die AWS Lambda Funktion konfiguriert.
Secret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Ref SecretName
KmsKeyId: !GetAtt Key.Arn
SecretString: superluminar.io
SecretRotationSchedule:
Type: AWS::SecretsManager::RotationSchedule
Properties:
SecretId: !Ref Secret
RotationLambdaARN: !GetAtt SecretRotationHandler.Arn
RotationRules:
AutomaticallyAfterDays: !Ref RotationPeriodInDays
Für viele hauseigene Dienste bietet AWS eine vorbereitete Integration zur automatischen Rotation. So lassen sich Zugangsdaten für Amazon RDS oder Amazon DocumentDB direkt austauschen. Zusätzlich kann die Rotation aber auch von einer generischen AWS Lambda Funktion ausgeführt werden.
AWS Lambda zur Rotation
Der AWS Secrets Manager ruft die hinterlegte Lambda Funktion für eine Rotation viermal auf und übergibt als Steps
Parameter vier Werte: createSecret
, setSecret
, testSecret
und finishSecret
.
Generischer Ablauf
Der generische Ablauf zur Rotation sieht in den einzelnen Schritten folgende Logik vor:
createSecret
legt eine neue Version im Secrets Manager alsAWSPENDING
an.setSecret
aktualisiert den neuen Wert in möglichen Drittsystemen.testSecret
verwendet die VersionAWSPENDING
zum Test bei Drittsystemen.finishSecret
markiert die VersionAWSPENDING
alsAWSCURRENT
.
Mit den vorgegebenen Namen der Versionen sichert AWS eine Integration in die AWS Management Console. Die Rotation kann dank des definierten Ablaufs jederzeit manuell per Browser gestartet werden.
AWS Lambda Quellcode
Die verwendete Lambda Funktion generiert als einfachste Rotation immer wieder eine neue zufällige Zeichenkette; daher sind die Schritte setSecret
und testSecret
in diesem Beispiel auch nicht relevant. Das mindert die Komplexität natürlich etwas.
Um eine neue Version im Schritt createSecret
anzulegen, genügt ein Aufruf mit dem AWS SDK:
// JavaScript: createSecret
(ClientRequestToken, SecretId) => {
return SM.putSecretValue({
ClientRequestToken,
SecretId,
SecretString: Math.random().toString(36).substring(7),
VersionStages: ["AWSPENDING"],
}).promise();
};
Im Schritt finishSecret
ist etwas mehr Logik notwendig. Für die übergebene SecretId
muss die vorhandene ID der Version AWSCURRENT
ermittelt werden, damit diese durch den temporären Wert der Version AWSPENDING
ersetzt werden kann.
// JavaScript: finishSecret
async (MoveToVersionId, SecretId) => {
const { VersionIdsToStages } = await SM.describeSecret({
SecretId,
}).promise();
let RemoveFromVersionId = "";
Object.keys(VersionIdsToStages).forEach((version) => {
if (VersionIdsToStages[version].indexOf("AWSCURRENT") >= 0) {
RemoveFromVersionId = version;
}
});
await SM.updateSecretVersionStage({
SecretId,
VersionStage: "AWSCURRENT",
RemoveFromVersionId,
MoveToVersionId,
}).promise();
};
Die beiden knappen Funktionen lassen sich in AWS CloudFormation mit einer AWS::Serverless::Function
Ressource und dem Parameter InlineCode
verwenden. So kann auf die Verwendung von externen Dateien verzichtet werden.
CloudFormation Template
Die gesamte Infrastruktur für die automatische Rotation im AWS Secrets Manager lässt sich mit AWS CloudFormation verwalten. Mit allen Ressourcen und zwei flexiblen Parametern kann daraus eine wiederverwendbare Blaupause erstellt werden.
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Manage secret with Secrets Manager and RotationSchedule
Parameters:
SecretName:
Type: String
Description: Secret Name
Default: /my/secret
RotationPeriodInDays:
Description: Secret Rotation Period in Days
Type: Number
Default: 10
Resources:
Key:
Type: AWS::KMS::Key
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
Properties:
KeyPolicy:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- kms:*
Resource: "*"
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
- Sid: "Allow use of the key"
Effect: Allow
Principal:
AWS: !GetAtt LambdaExecutionRole.Arn
Action:
- "kms:Encrypt"
- "kms:Decrypt"
- "kms:ReEncrypt*"
- "kms:GenerateDataKey*"
- "kms:DescribeKey"
Resource: "*"
Secret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Ref SecretName
KmsKeyId: !GetAtt Key.Arn
SecretString: superluminar.io
SecretRotationSchedule:
Type: AWS::SecretsManager::RotationSchedule
Properties:
SecretId: !Ref Secret
RotationLambdaARN: !GetAtt SecretRotationHandler.Arn
RotationRules:
AutomaticallyAfterDays: !Ref RotationPeriodInDays
SecretRotationHandler:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
Timeout: 120
Role: !GetAtt LambdaExecutionRole.Arn
InlineCode: |
const AWS = require('aws-sdk')
const SM = new AWS.SecretsManager({apiVersion: '2017-10-17'});
const cycle = {
/**
* Create new secret and store as "AWSPENDING" stage
*/
createSecret: (ClientRequestToken, SecretId) => {
return SM.putSecretValue({
ClientRequestToken,
SecretId,
SecretString: Math.random().toString(36).substring(7),
VersionStages: ['AWSPENDING']
}).promise();
},
/**
* Move "AWSCURRENT" stage pointer to version with current "AWSPENDING" stage
*/
finishSecret: async (MoveToVersionId, SecretId) => {
const { VersionIdsToStages } = await SM.describeSecret({ SecretId }).promise()
let RemoveFromVersionId = '';
Object.keys(VersionIdsToStages).forEach(
version => {
if (VersionIdsToStages[version].indexOf('AWSCURRENT') >= 0) {
RemoveFromVersionId = version
}
}
)
await SM.updateSecretVersionStage({
SecretId,
VersionStage: 'AWSCURRENT',
RemoveFromVersionId,
MoveToVersionId
}).promise();
}
}
exports.handler = async function(event, context) {
try {
await cycle[event.Step](event.ClientRequestToken, event.SecretId)
} catch (e) {
console.log(`Unable to handle step: ${event.Step}`)
}
}
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Path: "/"
Policies:
- PolicyName: access-logs
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:*
Resource: arn:aws:logs:*:*:*
- PolicyName: access-secretsmanager
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- secretsmanager:DescribeSecret
- secretsmanager:PutSecretValue
- secretsmanager:UpdateSecretVersionStage
Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}㊙️${SecretName}-*
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt SecretRotationHandler.Arn
Action: lambda:InvokeFunction
Principal: secretsmanager.amazonaws.com
Das Template kann durch die Parameter SecretName
und RotationPeriodInDays
konfiguriert werden und lässt sich somit auch ideal wiederverwenden.
Schnellstart
Die Benutzung über AWS CloudFormation lässt sich sogar so weit vorbereiten, dass alle Komponenten über eine simple URL angelegt werden können. So ist es nur ein Click um das gezeigte Beispiel in AWS Region eu-central-1 zu deployen!