I tried the token automatic rotation feature after AWS Secrets Manager managed external secrets became compatible with GitLab

I tried the token automatic rotation feature after AWS Secrets Manager managed external secrets became compatible with GitLab

I tried configuring rotation and immediate execution for GitLab access tokens using AWS Secrets Manager managed external secrets. Token rotation is completed with just IAM role and secret configuration, without writing any Lambda functions. I will summarize the setup steps and verification results.
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 July 6, 2026 update, Paddle and GitLab were added to the list of supported SaaS providers.

https://aws.amazon.com/about-aws/whats-new/2026/07/aws-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 auto-rotates GitLab tokens

Compared to the traditional approach, the differences are as follows.

Aspect Traditional (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
Audit 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 fees 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

By specifying --type GitLabAccessToken, the secret is recognized as a managed external secret.

The SecretString has the following format.

Key Description
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 earlier. The --rotation-lambda-arn used in standard rotation is not needed.
  • --external-secret-rotation-metadata: Specifies metadata related to the new token's expiration. In this case, daysToExpiry=30 was set.
  • --rotation-rules: The interval for automatic rotation.
  • --rotate-immediately: Executes rotation at the same time as the configuration.

Verifying Rotation Results

Checking Version Stages

After rotation completes, check the version stages using 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 moved to AWSPREVIOUS. The coexistence of AWSPENDING and AWSCURRENT in the same version is covered in the Notes section.

Changes in Token ID

Check the contents of the new token using 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 token ID held by Secrets Manager changed from 25439804 to 25439869. Validity on the GitLab side is confirmed via an API call described below.

Verifying the New Token's Validity

Call the GitLab API with the rotated token 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 the Old Token is Revoked

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 GitLab's rotate API, the issuance of a new token and the revocation of the old token were executed as a single process. This is the expected behavior per the rotate API specification.

Note that it took approximately 43 seconds from the start of rotation to confirming the AWSCURRENT promotion on the Secrets Manager side.

GitLab API Operations Using the Rotated Token

Verify that the rotated token 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 standard Lambda rotation, the following characteristics apply.

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

Since who performed the rotation, when, 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 the effect of daysToExpiry could not be determined in this verification.

Although the GitLab rotate API allows specifying expires_at, since the original token's expiration and the result of daysToExpiry=30 resulted in the same date in this verification, it was not possible to determine which value Secrets Manager prioritized. Verifying in a case where daysToExpiry and the original token's expiration date differ would allow for a clearer understanding of the behavior.

Remaining AWSPENDING Stage

Even after rotation completed, AWSPENDING and AWSCURRENT coexisted in the same version. In common sample implementations of Lambda rotation, AWSPENDING is removed in the finishSecret step, but in this verification, this state remained.

If an application explicitly specifies VersionStage=AWSCURRENT when calling GetSecretValue, this has no impact on behavior.

Choosing Between self-rotation and admin-assisted

This verification used the self-rotation method (rotation using the token's own permissions). 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: An administrator token is stored in a separate secret, and its permissions are used 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 AWS Secrets Manager's managed external secrets, we configured automatic rotation for a GitLab Personal Access Token and verified immediate rotation. Lambda function development and maintenance are not required; rotation works through an IAM role, a secret, and rotation configuration. On the GitLab side, the old token was revoked and a new token was issued via the rotate API. On the Secrets Manager side, it took approximately 43 seconds from the start of rotation to confirming the AWSCURRENT promotion.

The cost follows standard Secrets Manager pricing, with a base secret fee of $0.40 per month and API call fees (official pricing page, as of the time this article was written). For use cases that do not continuously 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. If you are rotating tokens for these SaaS providers using Lambda or manually, consider migrating to managed external secrets.

Share this article

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