How to Use AWS SSM Parameter Store in GitLab CI pipeline to Fetch Secrets via OIDC
A drop-in GitLab CI template that swaps long-lived AWS keys for short-lived OIDC credentials, cutting your secrets bill by up to 70%, with…
How to Use AWS SSM Parameter Store in GitLab CI pipeline to Fetch Secrets via OIDC
A drop-in GitLab CI template that swaps long-lived AWS keys for short-lived OIDC credentials, cutting your secrets bill by up to 70%, with almost no setup.
If you run GitLab CI on AWS, you’ve probably hit this wall: GitLab’s native AWS secrets integration only supports AWS Secrets Manager. If your team already standardized on AWS SSM Parameter Store (for config, feature flags, or just because it’s free), you’re stuck either paying for Secrets Manager anyway or hand-rolling your own credential-fetching logic in every pipeline.
I built [gitlab-ci-secrets-aws-ssm-parameter-store](https://github.com/abdullahkhawer/gitlab-ci-secrets-aws-ssm-parameter-store) to close that gap: a reusable GitLab CI template that pulls secrets from SSM Parameter Store using short-lived OIDC credentials, with zero static AWS keys stored in GitLab.
It’s easy to use: Create the AWS IAM resources once, include the template, and define which secrets each job needs. For the simplest case, that’s it. No per-job YAML at all.
Why SSM Parameter Store over Secrets Manager?
The functional difference is minor. Both store and encrypt values. The cost difference is not.
- Storage is free. Standard SSM parameters cost nothing to store; Secrets Manager charges $0.40/secret/month.
- API pricing is identical: $0.05 per 10,000 calls on both.
- At scale, it adds up fast. 500 secrets accessed by 1,000 pipelines a day (~15M API calls/month) costs roughly:
- SSM Parameter Store: ~$75/month
- Secrets Manager: ~$275/month ($200 storage + $75 API)
That’s a 72% saving, driven entirely by the per-secret storage fee. If your infra already uses SSM for configuration, keeping secrets in the same store also means one IAM model, one set of path-based policies, and one place to look, instead of splitting your config and secrets across two AWS services.
The core idea: OIDC instead of static keys
The template never stores an AWS access key or secret key in GitLab. Instead, it uses GitLab’s native OIDC support (id_tokens), available since GitLab 15.7:
GitLab CI job
│
├─ 1. GitLab issues a short-lived OIDC JWT
│
├─ 2. Job calls sts:AssumeRoleWithWebIdentity with that JWT
│ → AWS validates it against the registered OIDC IdP
│ → AWS returns temporary credentials (15-minute lifetime)
│
├─ 3. For each AWS_SSM_PARAM_<NAME> variable defined for the job,
│ → the matching SSM parameter path is fetched
│ → SecureString values are decrypted automatically
│
├─ 4. Each parameter value is exported as <NAME>
│
└─ 5. Temporary credentials are deleted immediately after
No long-lived IAM user, nothing to rotate, nothing to leak from a GitLab variable dump. And jobs that don’t declare any AWS_SSM_PARAM_* variables make zero AWS calls. The template is a no-op until you opt a job in.
Setting it up
Three steps, only the first of which touches AWS directly: deploy IAM resources once, set a few GitLab variables, and include the template. Everything after that is opt-in per job.
1. Deploy the IAM side with Terraform
The repo ships a small Terraform module that creates:
- The GitLab OIDC identity provider in AWS (or reuses an existing one)
- An IAM role scoped to
sts:AssumeRoleWithWebIdentity, restricted by the JWT'saudclaim and, optionally, itssubclaim (project/branch) - A least-privilege IAM policy granting
ssm:GetParameteron exact parameter ARNs only (noresource: "*", no recursive path fetching)
cd terraform
cp terraform.tfvars.example terraform.tfvars
# edit terraform.tfvars with your role name, region, and parameter ARNs
terraform init
terraform plan
terraform apply
The output gives you the role_arn for the next step.
Want to lock the role down to a single project and branch?
Set allowed_sub:
allowed_sub = "project_path:mygroup/myproject:ref_type:branch:ref:master"
Wildcards work too. project_path:mygroup/*:ref_type:branch:ref:* allows any branch in any project under a group. Leave it empty and any project with a valid OIDC token from your GitLab instance can assume the role, which is fine for a private GitLab instance but too loose for gitlab.com.
2. Configure GitLab CI/CD variables
In Settings → CI/CD → Variables, add:
SSM_ROLE_ARN=arn:aws:iam::123456789012:role/gitlab-ci-ssm-reader(Protected, Masked)SSM_AWS_REGION=eu-west-1SSM_OIDC_AUDIENCE=[https://gitlab.com](https://gitlab.com)AWS_SSM_PARAM_DB_PASSWORD=/myapp/prod/db_password(Protected, Masked)
3. Include the template
include:
- remote: 'https://raw.githubusercontent.com/abdullahkhawer/gitlab-ci-secrets-aws-ssm-parameter-store/master/templates/ssm-secrets.gitlab-ci.yml'
4. Declare which secrets a job needs
You get three ways to wire up a secret, from zero YAML to full control:
Option 1: GitLab Settings only.
Add AWS_SSM_PARAM_DB_PASSWORD=/myapp/prod/db_password as a CI/CD variable and $DB_PASSWORD is available in every pipeline job automatically. No YAML changes at all.
Option 2: inline, per job.
job:
variables:
AWS_SSM_PARAM_DB_PASSWORD: "/myapp/prod/db_password"
Option 3: compose with your own before_script.
job:
variables:
AWS_SSM_PARAM_DB_PASSWORD: "/myapp/prod/db_password"
before_script:
- !reference [.fetch-ssm-secrets, before_script]
That’s it. No extends:, no anchors to copy into your own pipeline, no boilerplate. The exported variable name is always the suffix after AWS_SSM_PARAM_, exactly as written, so AWS_SSM_PARAM_API_KEY becomes $API_KEY regardless of how nested the underlying SSM path is.
What’s happening under the hood
The template is a single YAML anchor reused in both default.before_script (for Options 1 & 2) and a .fetch-ssm-secrets job template (for Option 3, where you already have a before_script and want to compose it in with !reference). It:
- Skips entirely if no
AWS_SSM_PARAM_*env vars are present in the job, so there are no wasted AWS calls and no wasted seconds. - Installs the AWS CLI on the fly if it’s missing from the runner image (
apt-get,apk,yum,dnf, or a curl+unzip fallback; Alpine gets AWS CLI v2 automatically on 3.17+, v1 on older releases). - Calls
sts assume-role-with-web-identitywith the GitLab-issued JWT, requesting a 15-minute session. - Writes the temporary credentials to
mktemp-created files (chmod 600), pointed at viaAWS_SHARED_CREDENTIALS_FILE/AWS_CONFIG_FILE, so a shared runner's existing~/.aws/credentials are never touched. Everything is cleaned up via atrap ... EXIT. - Loops over every
AWS_SSM_PARAM_*variable, callsaws ssm get-parameter --with-decryption, and writesexport NAME=valuelines to a private temp file. - Sources that file into the job’s shell, then deletes it immediately.
The OIDC token itself is never logged, and because each AWS_SSM_PARAM_* variable is its own GitLab CI/CD variable, you retain GitLab's normal per-variable masking and branch/tag protection: fine-grained control over exactly which jobs and branches can reach which secret.
Security posture, briefly
- No static AWS credentials in GitLab, ever. Only a role ARN, which is useless without a valid OIDC token from an authorized project/branch.
- 15-minute credential lifetime, deleted the moment the job is done with them.
- Least-privilege IAM: the Terraform module scopes
ssm:GetParameterto exact ARNs, not wildcarded paths, and only addskms:Decryptif you actually pass in customer-managed KMS key ARNs. - Protected + masked variables on
SSM_ROLE_ARNand everyAWS_SSM_PARAM_*mean only protected branches can trigger role assumption and secret values never hit job logs.
When this is (and isn’t) the right call
This is a good fit if you’re already leaning on SSM Parameter Store for configuration and don’t want to pay the Secrets Manager storage premium just to keep secrets in a separate silo, or if you specifically want path-based IAM policies over your secrets. If you need features Secrets Manager has and SSM doesn’t, such as automatic rotation or cross-region replication, this template won’t replace that; it’s purely a fetch mechanism, not a secrets lifecycle manager.
The full source, including the Terraform module and template, is on GitHub: **abdullahkhawer/gitlab-ci-secrets-aws-ssm-parameter-store** (Apache 2.0). Contributions and suggestions welcome.
Author: Abdullah Khawer (LinkedIn)
메타데이터
- post_id
- fe2902e995b6
- slug
- how-to-use-aws-ssm-parameter-store-in-gitlab-ci-pipeline-to-fetch-secrets-via-oidc-fe2902e995b6
- url
- https://medium.com/@abdullah-khawer/how-to-use-aws-ssm-parameter-store-in-gitlab-ci-pipeline-to-fetch-secrets-via-oidc-fe2902e995b6
- canonical_url
- https://medium.com/@abdullah-khawer/how-to-use-aws-ssm-parameter-store-in-gitlab-ci-pipeline-to-fetch-secrets-via-oidc-fe2902e995b6
- author_url
- https://medium.com/@abdullah-khawer
- status
- ok
- fetched_at
- 2026-07-26 10:52:39