I tried token automatic rotation using AWS Secrets Manager managed external secrets, which now supports 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 update on July 6, 2026, 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 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:
- 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
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-arnused in normal rotation is not required.--external-secret-rotation-metadata: Specifies metadata related to the new token's expiration. In this case,daysToExpiry=30was 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
rotationLambdaARNfield does not exist (because Lambda is not used) - Instead,
externalSecretRotationRoleArnandexternalSecretRotationMetadataare recorded clientRequestTokenmatches 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
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: 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.
