Protecting secrets in GitLab CI/CD variables starts with knowing what your pipelines hold
Secrets in a self-managed GitLab instance usually fall into a few groups: deploy credentials for production, API tokens for registries and package feeds, and cloud access keys that let a job create or change infrastructure. Keeping them out of job logs, fork pipelines and unauthorized jobs depends on three things working together: variables marked masked and protected so they are only exposed where they should be, hidden values for anything that must never be read back in the UI, and a decision about which secrets should stop living in GitLab at all. Start by listing every credential your pipelines use, the project or group that owns it, the branches and environments allowed to read it, and the systems it opens. That inventory decides everything that follows.
This section is the starting point for the page. It explains what each CI/CD variable setting does, when a team needs it, and where variables stop being the right container for a secret. It also covers external secrets managers such as HashiCorp Vault and OpenBao, which hold credentials and issue short-lived values to jobs, and CI/CD inputs, which replace pipeline variables for ordinary parameters that are not sensitive. Confirm the exact behavior of each setting against the GitLab version you run, because the flags and their limits are documented per release and differ in detail from one edition to another.
DeployOpen implements and hardens this setup inside your own infrastructure, including the GitLab instance, its runners and any Vault or OpenBao cluster, so secret values never leave networks you control. The first working session usually produces a written inventory, a variable and environment map, and a list of secrets to move to a secrets manager. Ask any implementation partner to confirm in writing which GitLab version and secrets manager they will configure, how access to the secrets manager is itself controlled and audited, and who in your organization owns each credential after handover.
How masked, hidden and protected variables differ
Masked variables replace matching values in job logs with a placeholder, so a value printed by a job does not appear in plain text in the log. This is a log display control and does not restrict who can read the value. Every script step in the job still receives the value as an environment variable.
- Log redaction only: the value remains available to any script, package or tool that runs inside the job.
- Format requirements: GitLab only masks values that meet its masking rules. Confirm the current rules in the GitLab variables documentation, because a value that does not qualify cannot be masked.
- Transformed output: a job that encodes, splits, truncates or reformats a value can print a form the mask does not match, such as a base64 string.
- Tool output: verbose HTTP clients, debug flags and error traces can print headers or request bodies that contain the value.
Test masking with a throwaway job that prints the value, then test each transformed form your scripts actually produce before you rely on the mask.
Hidden variables are stored so that their value cannot be viewed in the GitLab interface after they are saved. Users with access to the project can see that the variable exists and can replace or delete it, but they cannot read the current value. This protects against casual disclosure through the settings page and makes accidental copying harder.
- No readback: the value cannot be displayed after saving, so keep the authoritative copy in a password manager or secrets manager.
- Overwrite-based rotation: changing a credential means writing a new value, which makes the rotation record your responsibility.
- No effect on job access: pipelines still receive the value at runtime, so a malicious job can print it.
- Creation path: confirm in the documentation how values supplied by other means, such as trigger or API inputs, are handled, because they may not follow the same path.
As a check, confirm that a maintainer who opens the variables page cannot read the value, and that your rotation procedure writes the new value to your source of record first.
Protected variables are passed only to pipelines that run on protected branches or protected tags. A feature branch pipeline does not receive the value, so a merge request from an unreviewed branch cannot print it. The control depends entirely on how branch and tag protection is configured.
- Branch scoping: the value is available only to pipelines on the branches and tags you protect, so verify the protection patterns cover your release branches.
- Dependence on merge rules: who can push to or merge into a protected branch decides who can trigger a job that receives the value.
- Runner pairing: protected runners run only jobs on protected refs, which limits which machines hold the value during a deployment.
- Trusted branch risk: a maintainer or compromised account that merges a malicious change to a protected branch can still print the value.
Review the list of protected branches and tags, and confirm that no branch able to deploy is left unprotected.
File-type variables write their value to a temporary file on the runner, and the variable itself holds the path to that file. This suits credentials that tools read from disk, such as certificates, kubeconfig files and service account key files. The file is intended for that job and should be removed when the job finishes.
- Path, not content: the environment variable contains the file path, so scripts must pass the path explicitly to the tools that need it.
- Content exposure: a command such as
caton the file prints the secret. Confirm in the documentation whether masking applies to file contents, and do not assume it does. - Runner disk lifetime: on shell executors, the file sits on the host for the life of the job, so check file permissions and cleanup on the runner.
- Tool compatibility: some tools read credentials only from environment variables, so confirm each tool's input option before choosing this type.
Before you rely on a file-type variable, run a job that lists the runner's temporary directory and confirm the file is removed afterwards.
The attributes combine, and each one closes a different path. Masking handles accidental log output, hiding handles disclosure through the settings interface, protection handles branch scope, and file type handles tools that need a credential on disk. Choosing one attribute and assuming it covers the rest is the most common gap.
- Deployment credentials: combine protection with an environment scope and protected environments so that only release pipelines receive the value.
- Read-only API tokens: mask and hide the value, and protect it if pipelines on feature branches have no need for it.
- Cloud and database credentials: these are better moved to a secrets manager that issues short-lived access, which is covered in the sections on Vault and cloud secrets managers.
- Documentation check: confirm the masking rules, the file content behavior and the handling of non-UI inputs in the GitLab variables documentation for your version.
Before you finalize settings, list each variable with its attributes and environment scope, and confirm that the list matches the deployment and release process your auditors will review.
Which secret storage option fits which credential
This table compares GitLab-stored variables, CI/CD inputs and an external secrets manager fetched at job runtime. Read each row by asking where the value is stored, who can change it and whether it should ever appear in a pipeline definition. The main takeaway is that GitLab variables suit many job settings, while the most sensitive credentials usually belong in a dedicated manager, and exact behavior depends on your GitLab version.
| Option | What it protects | Where the value lives and who can change it | When it fits |
|---|---|---|---|
| Masked variable | Keeps the value out of job logs when a job prints it | Stored in project, group or instance CI/CD settings; users who manage those settings can change it | Tokens and passwords that jobs need as environment values. Confirm the value format rules for masking in your version. |
| Hidden variable | Stops the value from being displayed in the settings interface once saved | Stored in CI/CD settings; the saved value cannot be read back, only replaced | Credentials that jobs must use but no one needs to view again. Confirm hidden variable support and limits in your version. |
| Protected variable | Limits exposure to pipelines running on protected branches or tags | Stored in CI/CD settings; available only to pipelines on protected refs | Production deployment credentials that should run only from release branches. Pair with protected environments and runners. |
| File-type variable | Passes multi-line secrets such as certificates or key files to a tool | Stored in CI/CD settings; written to a file on the runner during the job | Kubeconfigs, service account keys or certificates that tools read from a path. Confirm how files are cleaned up on your runners. |
| CI/CD inputs | Non-secret parameters such as target environment, region or version | Declared in pipeline configuration and set per run; visible in pipeline configuration and history | Choosing a deploy target or image tag. Not designed for secrets, so keep values non-sensitive. |
| External secrets manager | Long-lived or high-impact credentials, with issuance, rotation and audit kept outside GitLab | Held in HashiCorp Vault, OpenBao or a cloud secrets manager; the job requests a value at runtime | Production cloud, database or signing credentials, or where auditors expect a central store. Confirm the authentication method between GitLab and the manager for your version. |
When Vault or OpenBao is worth the added moving parts
Masked, hidden and protected CI/CD variables keep most project secrets out of job logs, but they still store the value inside GitLab and hand it to every job that is allowed to read it. A secrets manager is worth its added moving parts when a secret is powerful enough that a leaked static copy would cause serious harm, such as production cloud keys, database credentials with write access and the signing keys used for releases. For those, decide whether the pipeline should receive a long-lived value at all, or whether it should request a credential at run time that expires within minutes or hours. Dynamic secrets and short-lived leases are the main reason to adopt Vault or OpenBao: the secrets engine creates a credential when a job asks for it, ties it to a lease, and revokes it automatically when the lease ends or when it is revoked in bulk. Keep the number of such secrets small so the operating cost stays proportionate to the risk.
Vault's licensing has changed, and that affects the decision. HashiCorp announced on August 10, 2023 that future releases would move from MPL-2.0 to the Business Source License 1.1, and IBM completed its acquisition of HashiCorp on February 27, 2025. Vault releases from 1.15.0 onward fall under the Business Source License terms named in the Vault LICENSE file, which bar competitive hosted or embedded offerings and convert each version to MPL-2.0 four years after publication. Ask your legal and procurement teams whether that license works for your use. If you need Vault Enterprise features such as performance replication, HSM support, namespaces or FIPS compliance, a license must be applied to the cluster, and HashiCorp does not publish Vault Enterprise prices, so the cost must come from a sales quote. Confirm the current terms with HashiCorp and IBM before you commit.
Setting up protected secrets, CI/CD inputs and external retrieval
This sequence moves GitLab CI/CD secrets out of broad project variables and into scoped, protected and externally retrieved credentials, so that pipelines receive only what each job needs and no secret value appears in job logs.
Inventory and classify every CI/CD variable
The project or group owner lists each variable in Settings, CI/CD, Variables, along with every pipeline parameter passed through rules or trigger payloads. Each entry is tagged as a credential, a sensitive value that should never be printed, or a plain configuration value. Record which branches, environments and jobs read it. The step is complete when every variable has an owner, a classification and a list of consumers. Check: the inventory matches the variable list shown in the GitLab UI for each group and project in scope, so nothing is held in an undocumented place.
Set protected and masked flags and scope environments
For each credential, the administrator enables the Protect variable option so it is exposed only to pipelines on protected branches and tags, and the Mask variable option so the value is hidden in job logs. Where the value must not be readable after it is saved, the Hidden option is used, which the GitLab documentation describes as preventing the value from being revealed in the UI after creation. Set the environment scope to the narrowest environment that needs the value. Masking has preconditions on value format, so confirm each value passes the checks before saving. Check: run a pipeline on an unprotected feature branch and confirm the protected variable is absent from the job environment.
Convert parameters to CI/CD inputs
Non-secret parameters such as region names, image tags chosen at run time, or deployment flags are moved out of pipeline variables and into declared CI/CD inputs in the pipeline configuration, using the spec:inputs block. Each input declares a type, a default where appropriate and an allowed set of values where the platform supports it. Developers pass values through the pipeline UI or trigger API, which leaves a validated record instead of an arbitrary variable. Confirm the GitLab version in use supports the input syntax before rollout. Check: a pipeline run with an invalid input value fails validation before any job starts, and the job log shows the resolved value rather than a credential.
Configure JWT authentication to the secrets manager
The secrets platform team configures the secrets manager to trust GitLab as an OpenID Connect identity provider, using the job ID token that GitLab issues to each job. Define one role per project or per protected branch, and bind each role to the token claims for that project path and ref, so a job from one repository cannot assume another's role. Keep the policies to read-only access on the paths the job needs. This setup applies to HashiCorp Vault or OpenBao, or a cloud secrets manager with OIDC federation. Confirm the exact claim names and token options against the current GitLab and secrets manager documentation. Check: a job from an unlisted project or branch receives an authentication error and no secret.
Retrieve secrets at job runtime and remove stored copies
Pipelines fetch the credential inside the job at the point of use, store it only in memory or a short-lived file, and never write it to artifacts or cache. Once the external retrieval works, the team deletes the matching project or group variables and revokes any long-lived tokens or static keys they contained, then rotates values that were ever exposed in a previous configuration. Record the rotation date and the owner for each credential. Check: searching the variable list shows no remaining copies of the moved credentials, and a test job confirms the value is fetched dynamically with a short lifetime.
Verify with a test pipeline and log review
The team runs a dedicated test pipeline that uses each protected, masked and externally retrieved secret, and that prints deliberately recognizable test values where masking would apply. Reviewers open the job logs, the job trace and any artifacts, and confirm that no secret value appears in plain text. They also confirm that a run from an unprotected branch cannot read protected values and that a run from an unlisted project cannot retrieve secrets. Store the results as evidence for the change record. Check: every test case has a recorded pass result, and any failure blocks rollout until the scope or role is corrected.
Risks that remain after masking and how to reduce them
Masked and protected variables reduce exposure, but they do not stop every leak. The risks below sit around the variable itself: what jobs print, which branches can read values, how far values are inherited, how long credentials live, where they end up after a job, and who can see them as an administrator. Each card names a mitigation and the related hardening topic to check next.
Job log leakage through debug output
Masking hides a value only when GitLab recognizes it exactly as printed. Commands such as set -x, verbose tool flags, or scripts that echo a transformed or encoded form of a secret can write it to the job log in a shape masking does not catch. Review scripts that print environment state, and treat job logs as readable by anyone who can view the pipeline. Confirm the masking rules in your GitLab version against a test job before relying on them.
Unprotected branches and merge requests
Variables marked protected are exposed only to pipelines on protected branches and tags. Merge request pipelines from forks or unprotected branches can still run jobs that use other variables, so a contributor may be able to write a job that prints or sends a value. Check which variables are unprotected, which runners execute merge request jobs, and whether production deployments depend on a protected environment rather than on branch names alone.
Over-broad group and project inheritance
Variables defined at group or instance level flow into every project beneath them, including projects that never needed the value. A single broad variable can reach a sandbox project or an archived repository with weaker controls. Audit inherited variables, scope each one to the narrowest project or environment that uses it, and review who holds maintainer rights on those projects, since they can change how a job reads a value.
Long-lived static credentials
A static password or access token stored in a variable stays valid until someone rotates it, and copies of it persist in forks, logs and backups. Prefer short-lived credentials issued at job runtime from a secrets manager, and set rotation dates for the credentials you must keep. Where the value must remain in GitLab, record its owner, scope and rotation schedule so a leaked value has a known expiry.
Secrets reaching artifacts and caches
A job can write a secret into a file that later becomes an artifact, a cache entry, a test report or a container layer. Artifacts are downloadable by users with access to the project, and caches are shared between jobs on the same runner. Restrict artifact paths to build outputs, exclude configuration files from caches, and scan artifacts before publishing them. Related controls sit in runner isolation for jobs that share a host.
Administrators with broad read access
Instance administrators can reach projects, groups and job data that their team roles do not expose, so a compromised or careless administrator account exposes more than one project. Keep the number of full administrators small, require re-authentication for administrative actions, and review who can change CI/CD settings. Pair these checks with the Admin Mode and runner isolation topics in this group.
What buyers ask before moving GitLab secrets to a secrets manager
Are masked and protected CI/CD variables enough for our secrets?
For many teams, masked and protected variables are a reasonable first control, but they are not a secrets manager. GitLab stores these values in the instance, so anyone with maintainer access to a project or group can still read them through the pipeline if they can run a job that echoes them. Masking hides values in job logs, protected variables limit exposure to protected branches and tags, and hidden variables block reads in the UI. Long-lived, high-impact credentials such as production cloud keys or database passwords usually benefit from a dedicated store that issues short-lived credentials. Ask your team which secrets could cause the most damage if leaked, then decide which ones justify that extra step.
What operational work does moving secrets to Vault or OpenBao add?
The main addition is a new stateful service your team must run. Vault and OpenBao both need an initialization and unseal process, a highly available storage backend, backups of that storage, certificate management, audit logging and upgrades. OpenBao documents integrated storage (Raft) and PostgreSQL as its officially supported high availability backends, with an active node and standby nodes. Pipelines also need an authentication method, such as JWT/OIDC tied to GitLab job identity or AppRole for servers, and every job must be updated to fetch secrets at runtime. Plan for that rollout work and for a runbook for a sealed or unavailable cluster, since pipelines that depend on it will stop when it is down.
How much does a Vault Enterprise license cost?
HashiCorp does not publish Vault Enterprise prices. Its pricing pages describe pay-as-you-go, flex and Enterprise self-managed commercial models, and direct self-managed Enterprise buyers are sent to sales for a quote. The cost depends on the deployment model, the features you need and the scale you run, so request a written quote from HashiCorp that states the model, term and included features. Ask whether you need Enterprise features at all. Community Edition binaries are offered, and OpenBao is an MPL-2.0 community fork with its own support model. Compare total cost, including the internal time to operate whichever option you pick.
What does the secrets manager need from our identity, network and on-call teams?
Several teams share the work. Identity administrators configure trust between GitLab and the secrets manager, typically through JWT/OIDC claims that map jobs to roles, and they manage any human login methods. Network teams must allow runners to reach the secrets manager, decide where it sits and restrict which networks can connect. Security teams define policies that limit each role to the paths it needs. Operations or on-call staff need alerts, backup restore practice and a defined response for seal events and failed leases. Confirm named owners for each of these before you start, because a secrets manager without an owner becomes a single point of failure.
Can our own team move CI/CD secrets to a secrets manager without a partner?
Often yes, if your team already runs stateful infrastructure, has experience with identity federation and can keep a rollout list of pipelines and secrets. The work is manageable in stages: inventory every variable, classify secrets by impact, stand up a test cluster, configure GitLab job authentication, migrate one low-risk project and then expand. The risky parts are usually the unknowns: undocumented variables, pipelines that echo secrets, and jobs that run on shared runners with broad network access. A partner can help by scoping and documenting these steps, but your team should be able to operate the result after handover. Assess your own staffing and skills before deciding.
How long does the migration take for our number of pipelines?
Duration rises with the number of pipelines, the number of distinct secrets, how many environments each pipeline targets and how many teams must sign off on changes. Each pipeline that reads a secret must be changed and tested, and sensitive projects usually need a review of who can run their jobs. Platform setup happens once, but migration is repeated per project group. Ask any provider for a schedule built from your inventory, with the pipeline count, secret count and environment count stated in the scope. Treat any duration given without that inventory as a guess.
What does a fixed-scope engagement for this work include?
Most DeployOpen engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts. For this topic, a written scope typically covers the inventory and classification of CI/CD variables, the design of the secrets manager deployment and its authentication, the GitLab job configuration changes for agreed projects, verification that secrets are not exposed in logs, and handover documentation. Ask whether the scope includes a backup and restore test for the secrets manager, runner network changes, and support after handover. Ongoing support, upgrades and continuous testing are available as separate retainers, so confirm which items are one-time and which recur.
What should we ask a vendor for in a written quote?
Ask for a quote that lists each deliverable, the environments covered, the number of pipelines and projects in scope, the secrets to migrate, and the assumptions about your infrastructure. Request separate lines for one-time setup, migration per project group, documentation and handover, and any recurring support. Ask which components you must license or operate yourself, such as Vault Enterprise, storage and backups. Confirm who handles incidents during the rollout and what changes require your approval. A quote that cannot be traced back to a written inventory deserves more questions before you sign.
Rolling out secret controls without breaking pipelines
Roll out secret controls in stages, starting with the changes that cost the least and remove the most exposure. The first stage applies protected and masked flags to existing CI/CD variables, which restricts them to protected branches and tags and hides their values from job logs. The second stage moves parameters that were passed as pipeline variables to CI/CD inputs, so that values are declared, typed and validated in the pipeline configuration instead of being injected through the variables list. Only after these two stages does the work turn to the highest-risk credentials, such as production deployment keys and cloud access, which move to Vault or a cloud secrets manager and are read at job runtime. The final stage removes the static copies, revoking old tokens and deleting the variables they replaced once the new path has run successfully for a full release cycle.
Test each change on a non-production project before it reaches the group or production repositories. Create a sandbox project under the same group, mirror the pipeline and runner tags, and apply the new flags or inputs there first. Protected variables only resolve on protected refs, so a job that worked on a feature branch may fail after the change, and that is the failure you want to see in a sandbox rather than during a release. Keep a short written test list covering a protected branch run, a merge request run and a tag run. Confirm the results in the project's CI/CD settings and job logs before promoting the change. Also confirm with your platform team which GitLab version and runner versions you run, because behavior around inputs and secrets providers can differ between releases, and check the current guidance in the GitLab hardening recommendations before assuming a setting applies as described.
Record an owner for every secret before you change it. The register should list the variable or secret name, the scope (instance, group or project), the system it authenticates to, the people or team accountable for rotation, the environments it reaches, and the date of the last rotation. Without a named owner, nobody can approve a move to an external store or decide when a credential can be retired. Treat the GitLab hardening guidance as a work in progress that should be rolled out gradually, as its own documentation says, so schedule each stage as a tracked change with a rollback plan, and review the register at each stage gate. Ask your implementation partner or internal team to show how the register will be kept current, because a list that drifts out of date is the most common reason static copies survive a migration.
How DeployOpen scopes and delivers CI/CD secrets protection
DeployOpen plans, implements, hardens, documents and supports self-hosted GitLab inside your own infrastructure, including how CI/CD variables, masking, protected scopes and external secrets managers are set up. We start from an inventory of what your pipelines actually handle, then agree the work in writing before any change is made to your instance.
How we scope
- We inventory which CI/CD variables exist, which projects and groups define them, and which jobs read them.
- The written scope names which secrets move to a secrets manager and which stay as masked or protected variables.
- Engagements are fixed-scope, with deliverables and timelines agreed in writing before work starts.
How we build
- We configure masked, hidden and protected variables and restrict them to protected branches and tags.
- We convert pipeline parameters to CI/CD inputs and wire job authentication to your chosen secrets manager.
- We test that jobs on unprotected branches cannot read production credentials, and record the results.
How we hand over
- You receive a variable inventory, access rules, and a runbook for rotating each credential class.
- Configuration is documented so your team can review changes through its own merge request process.
- We walk your platform and security staff through the design and the verification evidence.
How we support
- Ongoing support, upgrades and continuous testing are available as a retainer after handover.
- Retainers can cover review of new projects, rotation checks and changes to GitLab or secrets manager versions.
- Ask any provider to confirm which GitLab and secrets manager versions their guidance was tested against.

