I tried the token automatic rotation feature after AWS Secrets Manager managed external secrets became compatible with GitLab
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.
With the July 6, 2026 update, Paddle and GitLab were added to the list of supported SaaS providers.
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:
- AWS Secrets Manager — Managed external secrets
- AWS Secrets Manager — GitLab access token
- GitLab — Rotate a personal access token (API)
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-arnused in standard rotation is not needed.--external-secret-rotation-metadata: Specifies metadata related to the new token's expiration. In this case,daysToExpiry=30was 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
rotationLambdaARNfield is absent (because no Lambda is used) - Instead,
externalSecretRotationRoleArnandexternalSecretRotationMetadataare recorded - The
clientRequestTokenmatches 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
apiscope. The GitLab rotate API is also said to be executable with theself_rotatescope (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.
