I tried token automatic rotation using AWS Secrets Manager managed external secrets, which now supports GitLab

I tried token automatic rotation using AWS Secrets Manager managed external secrets, which now supports GitLab

I tried the rotation configuration and immediate execution of GitLab access tokens using AWS Secrets Manager managed external secrets. Token rotation is completed with IAM role and secret configuration without writing Lambda. I will summarize the setup steps and the results of the operation verification.
2026.07.07

This page has been translated by machine translation. View original

Introduction

AWS Secrets Manager has a feature called "managed external secrets." It is a mechanism that manages external SaaS secrets on the Secrets Manager side and enables automatic rotation without writing a Lambda function.

https://dev.classmethod.jp/articles/update-aws-secrets-manager-managed-external-secrets/

With the update on July 6, 2026, Paddle and GitLab were added to the list of supported SaaS providers.

https://aws.amazon.com/jp/about-aws/whats-new/2026/07/secrets-manager-managed-external-secrets-paddle-gitlab/

There are several methods for integrating GitLab with AWS, each with a different direction.

Method Direction Use Case
GitLab OIDC → AWS IAM Role GitLab → AWS Secretless access to AWS resources from CI/CD
GitLab CI/CD aws_secrets_manager GitLab → AWS Reference Secrets Manager values within CI/CD jobs
Managed external secrets (this article) AWS → GitLab AWS manages and automatically rotates GitLab tokens

Compared to conventional methods, the differences are as follows.

Perspective Conventional (manual or custom Lambda) Managed external secrets
Rotation implementation Custom Lambda development & maintenance Configuration only (no Lambda required)
Old token revocation Custom implementation New and old tokens handled together via GitLab rotate API
Auditing CloudTrail + custom logs AWS-side operations recorded in CloudTrail
IP restrictions Manual NAT IP management AWS managed prefix list
Cost Lambda execution + development effort Secrets Manager charges only

※ The IP restrictions row is based on official documentation and was not verified in this validation.

In this article, we create a managed external secret targeting a GitLab Personal Access Token, and verify the behavior of automatic rotation configuration and immediate rotation.

Reference documentation:

Verification Details

Prerequisites

Creating a GitLab Personal Access Token

Create a Personal Access Token in GitLab. The scope was set to api.

  • Token Name: aws-secrets-manager-test
  • Scopes: api
  • Expires: 2026-08-06

After creation, note the Token ID (in this case 25439804) and the token value.

Creating an IAM Policy

Define the permissions required for the Secrets Manager rotation process.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowRotationAccess",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:DescribeSecret",
        "secretsmanager:GetSecretValue",
        "secretsmanager:PutSecretValue",
        "secretsmanager:UpdateSecretVersionStage"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "secretsmanager:resource/Type": "GitLabAccessToken"
        }
      }
    },
    {
      "Sid": "AllowPasswordGenerationAccess",
      "Effect": "Allow",
      "Action": ["secretsmanager:GetRandomPassword"],
      "Resource": "*"
    }
  ]
}

The Condition specifies "secretsmanager:resource/Type": "GitLabAccessToken". This ensures that the permissions of this policy apply only to managed external secrets (GitLabAccessToken type) and do not affect regular secrets.

aws iam create-policy \
  --policy-name SecretsManagerGitLabRotationPolicy \
  --policy-document file://policy.json

Creating an IAM Role

The Secrets Manager service assumes this role to execute the rotation process.

Trust Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SecretsManagerPrincipalAccess",
      "Effect": "Allow",
      "Principal": {"Service": "secretsmanager.amazonaws.com"},
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {"aws:SourceAccount": "123456789012"},
        "ArnLike": {"aws:SourceArn": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:*"}
      }
    }
  ]
}
aws iam create-role \
  --role-name SecretsManagerGitLabRotationRole \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy \
  --role-name SecretsManagerGitLabRotationRole \
  --policy-arn arn:aws:iam::123456789012:policy/SecretsManagerGitLabRotationPolicy

Creating the Secret

Create the secret by specifying --type GitLabAccessToken.

aws secretsmanager create-secret \
  --name test/gitlab/pat-managed \
  --type GitLabAccessToken \
  --secret-string '{"token":"glpat-****","tokenId":"25439804","gitlabUrl":"https://gitlab.com"}' \
  --region ap-northeast-1

Specifying --type GitLabAccessToken causes it to be recognized as a managed external secret.

The SecretString has the following format.

Key Content
token GitLab access token value
tokenId Numeric ID of the token (can be confirmed via the GitLab /personal_access_tokens API)
gitlabUrl URL of the GitLab instance

Configuring and Running Rotation

Configure rotation for the created secret and execute it immediately.

aws secretsmanager rotate-secret \
  --secret-id test/gitlab/pat-managed \
  --external-secret-rotation-role-arn arn:aws:iam::123456789012:role/SecretsManagerGitLabRotationRole \
  --external-secret-rotation-metadata '[{"Key":"daysToExpiry","Value":"30"}]' \
  --rotation-rules '{"AutomaticallyAfterDays":30}' \
  --rotate-immediately \
  --region ap-northeast-1

Key points:

  • --external-secret-rotation-role-arn: Specifies the IAM role created in advance. The --rotation-lambda-arn used in normal rotation is not required.
  • --external-secret-rotation-metadata: Specifies metadata related to the new token's expiration. In this case, daysToExpiry=30 was set.
  • --rotation-rules: Interval for automatic rotation.
  • --rotate-immediately: Executes rotation at the same time as the configuration.

Verifying the Rotation Results

Checking the Version Stage

After rotation is complete, check the version stage with describe-secret.

aws secretsmanager describe-secret \
  --secret-id test/gitlab/pat-managed \
  --query 'VersionIdsToStages' \
  --region ap-northeast-1
{
  "08d7dd1e-e299-4aeb-8dc9-693ceb42e6f1": ["AWSCURRENT", "AWSPENDING"],
  "9c1a8d5b-bb3f-4ae3-820b-48a14e501cac": ["AWSPREVIOUS"]
}

The new version (08d7dd1e-...) has been promoted to AWSCURRENT, and the original version (9c1a8d5b-...) has been moved to AWSPREVIOUS. The coexistence of AWSPENDING and AWSCURRENT in the same version is supplemented in the notes section.

Change in Token ID

Check the contents of the new token with get-secret-value.

aws secretsmanager get-secret-value \
  --secret-id test/gitlab/pat-managed \
  --region ap-northeast-1 \
  --query 'SecretString' --output text | jq .
{
  "token": "glpat-****",
  "tokenId": "25439869",
  "gitlabUrl": "https://gitlab.com"
}

The ID of the token held by Secrets Manager changed from 25439804 to 25439869. The validity on the GitLab side is confirmed via the API call described below.

Verifying the Validity of the New Token

Call the GitLab API with the token after rotation to verify its validity.

curl --header "PRIVATE-TOKEN: glpat-****" \
  "https://gitlab.com/api/v4/personal_access_tokens/self" | jq .
{
  "id": 25439869,
  "name": "aws-secrets-manager-test",
  "active": true,
  "scopes": ["api"],
  "expires_at": "2026-08-06",
  "created_at": "2026-07-07T06:38:11.770Z"
}

The new token is valid (active: true).

Verifying Revocation of the Old Token

Accessing with the pre-rotation token (Token ID: 25439804) returns a 401.

curl -s -o /dev/null -w "%{http_code}" \
  --header "PRIVATE-TOKEN: glpat-****" \
  "https://gitlab.com/api/v4/personal_access_tokens/self"
401

Via the GitLab rotate API, the issuance of the new token and revocation of the old token were executed as a single process. This is the expected behavior per the rotate API specification.

It took approximately 43 seconds from the start of rotation to confirmation of the AWSCURRENT promotion on the Secrets Manager side.

GitLab API Operations with the Token After Rotation

Verify that the token after rotation can be used not only for read operations but also for write operations.

Retrieving project information:

curl --header "PRIVATE-TOKEN: glpat-****" \
  "https://gitlab.com/api/v4/projects?owned=true&simple=true" | jq '.[0] | {id, name, web_url}'
{
  "id": 72894561,
  "name": "test-project",
  "web_url": "https://gitlab.com/personal-group/test-project"
}

Creating an issue:

curl --request POST \
  --header "PRIVATE-TOKEN: glpat-****" \
  --header "Content-Type: application/json" \
  --data '{"title":"Test issue from rotated token"}' \
  "https://gitlab.com/api/v4/projects/72894561/issues" | jq '{id, iid, title, state}'
{
  "id": 198765432,
  "iid": 1,
  "title": "Test issue from rotated token",
  "state": "opened"
}

Creating a Pipeline trigger:

curl --request POST \
  --header "PRIVATE-TOKEN: glpat-****" \
  --form "description=trigger" \
  "https://gitlab.com/api/v4/projects/72894561/triggers" | jq '{id, description, created_at}'
{
  "id": 5326232,
  "description": "trigger",
  "created_at": "2026-07-07T06:42:15.123Z"
}

All operations completed successfully.

Checking Events in CloudTrail

Check the RotateSecret event in CloudTrail.

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=RotateSecret \
  --max-results 1 \
  --region ap-northeast-1 \
  --query 'Events[0].CloudTrailEvent' --output text | jq .
Full CloudTrail event
{
  "eventVersion": "1.11",
  "eventTime": "2026-07-07T06:37:29Z",
  "eventSource": "secretsmanager.amazonaws.com",
  "eventName": "RotateSecret",
  "awsRegion": "ap-northeast-1",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::123456789012:assumed-role/your-role-name/your-role-name"
  },
  "requestParameters": {
    "secretId": "test/gitlab/pat-managed",
    "clientRequestToken": "08d7dd1e-e299-4aeb-8dc9-693ceb42e6f1",
    "rotationRules": {"automaticallyAfterDays": 30},
    "externalSecretRotationMetadata": [{"key": "daysToExpiry", "value": "30"}],
    "externalSecretRotationRoleArn": "arn:aws:iam::123456789012:role/SecretsManagerGitLabRotationRole",
    "rotateImmediately": true
  },
  "responseElements": {
    "versionId": "08d7dd1e-e299-4aeb-8dc9-693ceb42e6f1",
    "name": "test/gitlab/pat-managed",
    "arn": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:test/gitlab/pat-managed-Inxj3H"
  }
}

Compared to regular Lambda rotation, the following characteristics stand out.

  • The rotationLambdaARN field does not exist (because Lambda is not used)
  • Instead, externalSecretRotationRoleArn and externalSecretRotationMetadata are recorded
  • clientRequestToken matches the VersionId of the new version

Since who executed the rotation, when it was executed, and which role was used are automatically recorded in CloudTrail, it becomes easier to satisfy audit requirements.

Notes & Tips

daysToExpiry and the Actual Token Expiration

daysToExpiry: 30 was specified in --external-secret-rotation-metadata. The expires_at of the new token after rotation was 2026-08-06. This is the same date as the original token, and it also coincides with 30 days after the rotation date (2026-07-07), so this verification could not determine whether daysToExpiry had any effect.

The GitLab rotate API allows specifying expires_at, but in this verification the original token's expiration and the result of daysToExpiry=30 fell on the same date, making it impossible to determine which value Secrets Manager prioritized. Testing in a case where daysToExpiry and the original token's expiration differ would allow the behavior to be confirmed more clearly.

Remaining AWSPENDING Stage

Even after rotation was complete, AWSPENDING and AWSCURRENT coexisted in the same version. In typical sample implementations of Lambda rotation, the AWSPENDING stage is removed in the finishSecret step, but this state persisted in this verification.

If an application calls GetSecretValue with VersionStage=AWSCURRENT explicitly specified, there is no impact on behavior.

Choosing Between Self-Rotation and Admin-Assisted

This verification used the self-rotation method (rotation using the permissions of the token itself). There is also an admin-assisted method that specifies adminSecretArn.

  • Self-rotation: In this verification, it worked with a token having the api scope. The GitLab rotate API is also said to be executable with the self_rotate scope (unverified). Note that if the token itself expires, there is no recovery method.
  • Admin-assisted: Stores an admin token in a separate secret and uses those permissions to rotate the target token. This method allows rotation without depending on the target token itself.

Please choose based on your operational requirements.

Summary

Using managed external secrets in AWS Secrets Manager, we confirmed the automatic rotation configuration and immediate rotation of a GitLab Personal Access Token. No Lambda function development or maintenance is required; rotation operates through an IAM role, a secret, and rotation settings. On the GitLab side, the rotate API revoked the old token and issued a new one. On the Secrets Manager side, it took approximately 43 seconds from the start of rotation to confirmation of the AWSCURRENT promotion.

The pricing follows the standard Secrets Manager rates, incurring a base secret fee of $0.40 per month and API call charges (official pricing page, at the time of writing this article). For use cases that do not constantly reference secrets, this is a cost-effective option.

In addition to GitLab, BigID, Confluent Cloud, Datadog, MongoDB Atlas, Paddle, Salesforce, and Snowflake are also supported. For cases where SaaS tokens for these services are being rotated with Lambda or manually, please consider migrating to managed external secrets.

Share this article

AWSのお困り事はクラスメソッドへ