
Token-Zero: When you need a token to create a token
- Paul Thomas
- Gitlab , Access token , Ruby
- January 10, 2025
Table of Contents
When automating a GitLab installation, you’ll often need to create “token zero” - the first access token used to bootstrap your automation processes. Unlike the initial root password, this token is specifically scoped for API interactions during setup. This creates a chicken-and-egg situation: you need a token to make API calls, but you need to make an API call to create a token.
Understanding Access Tokens in Automated GitLab Installations
During automated deployments, multiple configuration steps require API authentication to create resources, set up runners, or configure integrations. The challenge lies in creating that very first access token when your automation needs to make its first API call.
A common solution is to manually create an initial access token with minimal required scopes (like api
and read_user
) through the GitLab UI or API after the first installation step. This token can then be securely stored and used by your automation tools to perform subsequent configuration tasks. Unlike the root password, this token should have carefully limited permissions aligned with the principle of least privilege.
Ideally, practices for handling this initial token include:
- Create it with only the necessary scopes for your automation
- Store it securely in your configuration management system or secrets manager
- Consider it a temporary bootstrap token and rotate it once your installation is complete
- Document its creation and usage as part of your installation process
The problem lies with the fact that this token is generally created using the GitLab UI, by a human, meaning that the token is immediately compromised from a visibility perspective - at least one human being has seen it.
However - it is possible to create a token using ruby:
begin
user = User.find_by_username('root')
token = PersonalAccessToken.create!(
user: user,
name: 'token-zero',
scopes: ['admin_mode','api'],
expires_at: '2025-12-31'
)
puts token.token
rescue => e
puts 'Error: ' + e.message
exit 1
end
You can wrap a script like this up in a bit of bash and include it somewhere in your userdata (or similar), here’s what works for me:
#!/bin/bash -ex
USER_NAME="root"
TOKEN_NAME="gitlab-admin-token"
EXPIRES_AT=$(date -u -d "+1 year" +"%Y-%m-%d")
SCOPES="'admin_mode','sudo','api','create_runner','manage_runner'"
TOKEN=$(sudo gitlab-rails runner "
begin
user = User.find_by_username('$USER_NAME')
token = PersonalAccessToken.create!(
user: user,
name: '$TOKEN_NAME',
scopes: [$SCOPES],
expires_at: '$EXPIRES_AT'
)
puts token.token
rescue => e
puts 'Error: ' + e.message
exit 1
end
")
if [ -z "$TOKEN" ]; then
echo "Failed to generate GitLab token"
exit 1
fi
SECRET_STRING=$(cat <<EOF
{
"gitlab_token": "$TOKEN",
"created_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"expires_at": "$EXPIRES_AT",
"scopes": "$SCOPES"
}
EOF
)
echo "Storing api key..."
aws secretsmanager put-secret-value \
--secret-id "${token_secret_id}" \
--secret-string "$SECRET_STRING" \
--region ${aws_region}
I store the token in AWS Secrets Manager, from where it can be picked up in subsequent deployment stages.
Having a clear process for handling this initial access token is crucial for maintaining security while enabling automated deployments.