Secrets in a pipeline

A secret is not a file

The Ruby release from lesson 59 needs an API key, and a key in the repository is a key anyone who clones the repository owns. The rule is simple: the repository holds code, the CI settings hold secrets, and the job receives them as environment variables.

Both systems call the same idea something different: GitHub Actions has secrets, GitLab CI has masked variables. In the job they arrive as ENV, exactly like the variables from lesson 40.

Read a secret without printing it

The first thing a script does with a token is check that it exists. Print its length, not its value: the length is enough to know the variable arrived, and a length in a log is not a leaked key.

API_TOKEN=abc123 ruby -e 'token = ENV["API_TOKEN"]; puts token ? token.length : "missing"'

6

ruby -e 'token = ENV["API_TOKEN"]; puts token ? token.length : "missing"'

missing

The same check belongs at the start of the release task: a missing token should stop the job in the first second, not after a build that cannot publish.

Both pipelines pass a variable

A GitHub workflow reads the secret into the step environment:

- name: Publish the gem run: gem push *.gem env: GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}

GitLab does the same with a protected variable named RUBYGEMS_API_KEY; the job needs no YAML for it, because the variable is already in the environment:

publish: stage: release script: - gem push *.gem

Parse either file the usual way; the secret is never in the file, only its name:

ruby -ryaml -e 'p YAML.load_file(ARGV[0])["jobs"]["release"]["steps"][-1]["env"]' .github/workflows/ci.yml

{"GEM_HOST_API_KEY" => "${{ secrets.RUBYGEMS_API_KEY }}"}

What to watch for

A secret that reaches a log is a secret that must be rotated. Masking in the CI hides the exact value, but a script that prints ENV.to_h or a failing HTTP request with headers can still write it to the job output. Read tokens by length or by a boolean, never by value.

Keep .env out of the repository. Locally a .env file is convenient; in the pipeline the variables come from the CI settings, and the two should never be mixed in one commit.

Next: One job, several Ruby versions

Article author: Arthur Isaev