AWS Secrets Manager Managed External Secrets Now Support GitLab, So I Tried Automatic Token Rotation
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.
In the July 6, 2026 update, Paddle and GitLab were added to the supported SaaS list.
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:
- AWS Secrets Manager — Managed external secrets
- AWS Secrets Manager — GitLab access token
- GitLab — Rotate a personal access token (API)
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 setdaysToExpiry=30this 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
rotationLambdaARNfield does not exist (because no Lambda is used) externalSecretRotationRoleArnandexternalSecretRotationMetadataare recorded insteadclientRequestTokenmatches 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
apiscope. GitLab's rotate API is also said to be executable with theself_rotatescope (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.
