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:
createSecretlegt eine neue Version im Secrets Manager alsAWSPENDINGan.setSecretaktualisiert den neuen Wert in möglichen Drittsystemen.testSecretverwendet die VersionAWSPENDINGzum Test bei Drittsystemen.finishSecretmarkiert die VersionAWSPENDINGalsAWSCURRENT.
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}:secret:${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!
