AWS Secrets Manager und automatische Rotation für Passwörter

Der 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 als AWSPENDING an.
  • setSecret aktualisiert den neuen Wert in möglichen Drittsystemen.
  • testSecret verwendet die Version AWSPENDING zum Test bei Drittsystemen.
  • finishSecret markiert die Version AWSPENDING als AWSCURRENT.

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!

Sebastian ist Senior Cloud Consultant bei superluminar GmbH, AWS Serverless Hero und AWS Certified Solutions Architect. Er schreibt hier auf Deutsch über AWS, Serverless, Software Entwicklung, Go, TypeScript und React. Englische Artikel sind auf seiner Webseite sbstjn.com oder auf Twitter unter @sbstjn zu finden.