AWS Secrets Manager Managed External Secrets Now Support GitLab, So I Tried Automatic Token Rotation

AWS Secrets Manager Managed External Secrets Now Support GitLab, So I Tried Automatic Token Rotation

I tried configuring and immediately running GitLab access token rotation with AWS Secrets Manager managed external secrets. No Lambda needed: IAM role and secret settings suffice. Covers setup steps and test 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 lets Secrets Manager manage secrets for external SaaS and rotate them automatically without writing a Lambda function.

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

In the July 6, 2026 update, Paddle and GitLab were added to the supported SaaS list.

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

There are several ways to integrate GitLab with AWS, and each has a different direction.

Method Direction Purpose
GitLab OIDC → AWS IAM Role GitLab → AWS Secretless access from CI/CD to AWS resources
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 with the conventional approach, there are the following differences.

Aspect Conventional (manual or custom Lambda) Managed external secrets
Rotation implementation Develop and maintain a custom Lambda Configuration only (no Lambda needed)
Old token revocation Self-implemented New and old tokens handled together via the GitLab rotate API
Auditing CloudTrail + custom logs AWS-side operations are recorded in CloudTrail
IP restriction Manually manage NAT IPs AWS managed prefix list
Cost Lambda execution + development effort Secrets Manager charges only

※ The IP restriction row is based on the official documentation and was not verified in this test.

In this article, I create a managed external secret for a GitLab Personal Access Token and verify the automatic rotation setup and the behavior of immediate rotation.

Reference documents:

What I Tested

Prerequisites

Creating a GitLab Personal Access Token

Create a Personal Access Token in GitLab. I specified api as the scope.

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

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

Creating the IAM Policy

Define the permissions required for Secrets Manager's 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 applies the permissions in this policy only to managed external secrets (the GitLabAccessToken type). Regular secrets are not affected.

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

Creating the IAM Role

The Secrets Manager service assumes this role to perform 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 makes it recognized as a managed external secret.

The SecretString has the following format.

Key Description
token The GitLab access token value
tokenId The numeric ID of the token (can be checked via GitLab's /personal_access_tokens API)
gitlabUrl The URL of the GitLab instance

Configuring and Running Rotation

Configure rotation for the created secret and run 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: Specify the IAM role created beforehand. --rotation-lambda-arn, which is used for normal rotation, is not needed
  • --external-secret-rotation-metadata: Specify metadata about the expiration of the new token. I set daysToExpiry=30 this time
  • --rotation-rules: The interval for automatic rotation
  • --rotate-immediately: Run rotation at the same time as the configuration

Checking the Rotation Results

Checking the Version Stages

After rotation completes, check the version stages 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-...) was promoted to AWSCURRENT, and the original version (9c1a8d5b-...) moved to AWSPREVIOUS. The fact that AWSPENDING and AWSCURRENT coexist on the same version is covered in the Notes section.

Change in Token ID

Check the content 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 token ID held by Secrets Manager changed from 25439804 to 25439869. Its validity on the GitLab side is confirmed with the API call described below.

Verifying the New Token Is Valid

Call the GitLab API with the post-rotation 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

Through GitLab's rotate API, issuing the new token and revoking the old token were executed as a single process. This matches the rotate API specification.

Incidentally, it took about 43 seconds from the start of rotation until I confirmed the promotion to AWSCURRENT on the Secrets Manager side.

GitLab API Operations with the Post-Rotation Token

I confirm that the post-rotation token can be used not only for reads but also for write operations.

Getting 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 of them worked correctly.

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 with normal Lambda rotation, it has the following characteristics.

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

Because who ran the rotation, when, and which role was used are automatically recorded in CloudTrail, it is easier to meet audit requirements.

Notes and Tips

daysToExpiry and the Actual Token Expiration

I specified daysToExpiry: 30 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 also matches 30 days after the rotation date (2026-07-07), so this test could not determine whether daysToExpiry had any effect.

GitLab's rotate API allows specifying expires_at, but in this test the original token's expiration and the result of daysToExpiry=30 happened to be the same date, so I could not determine which value Secrets Manager prioritized. Testing a case where daysToExpiry and the original token's expiration differ would make the behavior clearer.

AWSPENDING Stage Remaining

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

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

Choosing Between self-rotation and admin-assisted

This time I tested 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 test it worked with a token having the api scope. GitLab's rotate API is also said to be executable with the self_rotate scope (not verified). Note that there is no recovery method if the token itself has expired
  • admin-assisted: Store an administrator token in a separate secret and rotate the target token using that token's permissions. This method can rotate without depending on the target token itself

Choose according to your operational requirements.

Summary

Using AWS Secrets Manager managed external secrets, I verified the automatic rotation setup and immediate rotation of a GitLab Personal Access Token. No Lambda function development or maintenance is needed, and rotation works with just an IAM role, a secret, and rotation settings. On the GitLab side, the old token was revoked and a new token was issued through the rotate API. On the Secrets Manager side, it took about 43 seconds from the start of rotation until I confirmed promotion to AWSCURRENT.

Pricing is the standard Secrets Manager pricing, with a base secret fee of $0.40 per month plus API call charges (official pricing page, as of the time of writing). For use cases where the secret is not referenced constantly, it is a cost-effective choice.

Besides GitLab, BigID, Confluent Cloud, Datadog, MongoDB Atlas, Paddle, Salesforce, and Snowflake are also supported. If you are rotating tokens for these SaaS with Lambda or manually, consider migrating to managed external secrets.

Share this article

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