マスキングを信じていたのに base64 でトークンがログに残った
目標
変数が入ってくる場所の優先順位と環境スコープを実行で確認し、本番の秘密が入るジョブをmainに絞ったあとで、マスキングのないログに秘密が残る様子を見て、ログ漏えい検査器と参照固定検査器を作ります。
なぜ重要なのか
パイプラインは、リポジトリで最も強い権限を持って任意のコードを実行する場所です。同じ名前の変数が複数の場所から入ってくるとき、どの値が勝つかを知っておかないと、本番の値が上書きされる事故を防げず、環境スコープとブランチのルールで、秘密が入るジョブの数を減らす必要があります。マスキングは値とまったく同じ文字列だけを隠すので、形を変えた出力はそのまま残り、includeとイメージをラベルで参照すると、誰かがそのラベルを付け替える日に、自分たちのパイプラインが他人のコードを実行します。
ステップ
/root/glci-secretsをgitリポジトリにして、.gitignoreに.gitlab-ci-local/と.gitlab-ci-local-variables.ymlを置いてください。.gitlab-ci.ymlには、stages[build, deploy]、グローバルのvariablesREGION: yaml-global・LOG_LEVEL: yaml-global、ジョブshow(build、ジョブのvariablesLOG_LEVEL: yaml-job、echo "region=$REGION log=$LOG_LEVEL")を置きます。プロジェクト変数ファイル.gitlab-ci-local-variables.ymlには、REGION: project-var、LOG_LEVEL: project-varと、環境ごとに値が異なるDEPLOY_TOKEN(productionはprod-tok-7f3a9c、stagingはstg-tok-41be02、review/*はreview-tok-9d0c11)を置きます。設定だけをコミットしてください。showのログはregion=project-var log=project-varになるはずです。- ジョブを3つ追加してください(すべてdeployステージ)。
deploy-stagingはenvironmentstaging、deploy-prodはproduction、review-appはreview/$CI_COMMIT_REF_SLUGで、それぞれecho "<staging|prod|review> token-len=${#DEPLOY_TOKEN}"で、トークンの値ではなく長さだけを出力します。コミットして実行すると、3つのジョブがそれぞれ自分の環境のトークンの長さ(staging 14・prod 15・review 17)を出力するはずです。 deploy-prodにrules: - if: $CI_COMMIT_BRANCH == "main"を追加して、コミットしてください。採点ツールは、コピーのfeatureブランチでdeploy-prodが一覧になく、本番トークンが入るジョブ自体ができないかを確認します。- ジョブ
bad-debug(deploy、environmentはproduction、mainでだけ)を追加して、echo "debugging with $DEPLOY_TOKEN"とecho -n "$DEPLOY_TOKEN" | base64を実行させ、コミットしてください。実行したあと、このジョブのログ(.gitlab-ci-local/output/bad-debug.log)を/root/glci-secrets/leak-evidence.logにコピーし、このファイルを.gitignoreに追加してコミットされないようにしてください。 /root/glci-secrets/leak-scan.sh <저장소>を作ってください。リポジトリを一時的なコピーにしてコミットし、パイプラインを動かしたあとで、.gitlab-ci-local/output/*.logに、プロジェクト変数ファイルのうち名前にTOKEN・PASSWORD・SECRET・KEYが含まれる変数の値(8文字以上、環境別の値を含む)が、そのままかbase64の形で入っていれば、LEAK <잡> <변수>を1行ずつ(ソート済み)出力して3で終了します。なければOKを出力して0で、パイプラインが失敗すればERRORを出力して1で終了します(プレースホルダーは、順にリポジトリ、ジョブ名、変数名です)。元のリポジトリには痕跡を残しません。このリポジトリで実行すると、LEAK bad-debug DEPLOY_TOKENが出力されるはずです。bad-debugを削除し、代わりにジョブcheck-token(deploy、environmentはproduction、mainでだけ)を追加して、test -n "$DEPLOY_TOKEN" && echo token-presentだけを実行するようにしてください。コミットしたあと、leak-scan.shがOKを出力するはずです。/root/glci-secrets/pin-audit.sh <설정파일>を作ってください。includeのprojectの項目のrefが40桁のコミットSHAでない場合、remote includeの場合、あるいはcomponentが@<40자리 SHA>で終わらない場合はUNPINNED include <값>を、ジョブのimageに@sha256:のダイジェストがない場合はUNPINNED image <값>を、1行ずつ(ソート済み、重複なし)出力して3で終了し、なければOKを出力して0で終了します(プレースホルダーは、順に設定ファイル、40桁のSHA、値です)。projectの項目の値は<project>@<ref>と書きます。/root/glci-secrets/pin-sample.ymlにサンプル設定(課題本文の下にあるファイル例と同じ内容)を置き、結果を/root/glci-secrets/pin-report.txtに保存してください。
参考
- このVMにはGitLabサーバーもランナーもなく、gitlab-ci-local 4.75.1が.gitlab-ci.ymlをGitLabと同じルールで解釈し、シェルでジョブを実行します。
image:を書くとDockerで動かそうとするので使いません。保護変数・マスキング・CI_JOB_TOKEN・ランナーのタグ・マージリクエストパイプラインの作成はサーバーの機能なので、ここでは再現されません。 - 実行はリポジトリのルートで
gitlab-ci-local --shell-isolation --no-artifacts-to-source(ジョブごとに別の作業ディレクトリを使い、アーティファクトをリポジトリに書き戻さない)、ジョブ一覧はgitlab-ci-local --list-csv-all、解釈された設定はgitlab-ci-local --previewです。gitlab-ci-localはgitが追跡しているファイルだけをジョブに渡すので、ファイルを作ったらgit addしてください。採点ツールはリポジトリをコピーし、すべてのファイルをコミットしたあとで、同じツールでもう一度実行します。 .gitlab-ci-local-variables.ymlは、GitLabプロジェクト設定のCI/CD変数の代わりをします(環境スコープのvaluesに対応)。GitLabの保護変数・マスキング・隠し変数・CI_JOB_TOKENはサーバーの機能なので、再現しません。参考までに、GitLab CE 19.3.2を実際に立ち上げて試した結果(2026-09-15)、マスキングされた変数はログに[MASKED]と、CI_JOB_TOKENはglcbt-で始まる値として出力されました。- よくあるミスは、変数ファイルをコミットしてしまうことと、デバッグのためにset -xやenvの全体を出力してしまうことです。
- GitLab CI/CD variables(優先順位・保護・マスキング)・CI/CD job token・include・Environments・Protected branches
同じ名前の変数が4つ、どれが勝つか
/root/glci-secretsをgitリポジトリにして、.gitignoreに.gitlab-ci-local/と.gitlab-ci-local-variables.ymlを置いてください。.gitlab-ci.ymlには、stages[build, deploy]、グローバルのvariablesREGION: yaml-global・LOG_LEVEL: yaml-global、ジョブshow(build、ジョブのvariablesLOG_LEVEL: yaml-job、echo "region=$REGION log=$LOG_LEVEL")を置きます。プロジェクト変数ファイル.gitlab-ci-local-variables.ymlには、REGION: project-var、LOG_LEVEL: project-varと、環境ごとに値が異なるDEPLOY_TOKEN(productionはprod-tok-7f3a9c、stagingはstg-tok-41be02、review/*はreview-tok-9d0c11)を置きます。設定だけをコミットしてください。showのログはregion=project-var log=project-varになるはずです。
GitLabは、プロジェクト設定の変数をYAMLのジョブ変数・グローバル変数より優先します。パイプラインを実行するときに直接与えた値(ここでは--variable)は、それよりさらに優先されます。変数ファイルは秘密を含むので、リポジトリにアップロードしません。git check-ignoreで確認してください。
環境ごとに異なる秘密が入る
ジョブを3つ追加してください(すべてdeployステージ)。deploy-stagingはenvironmentstaging、deploy-prodはproduction、review-appはreview/$CI_COMMIT_REF_SLUGで、それぞれecho "<staging|prod|review> token-len=${#DEPLOY_TOKEN}"で、トークンの値ではなく長さだけを出力します。コミットして実行すると、3つのジョブがそれぞれ自分の環境のトークンの長さ(staging 14・prod 15・review 17)を出力するはずです。
変数に環境スコープを置くと、そのenvironmentで宣言したジョブにだけ値が入ります。ワイルドカードのスコープ(review/*)は、動的な環境名に合わせます。値が必要かどうかを確認するには、値の代わりに長さや存在の有無だけを出力します。
本番トークンが入るジョブはmainでだけ作る
deploy-prodにrules: - if: $CI_COMMIT_BRANCH == "main"を追加して、コミットしてください。採点ツールは、コピーのfeatureブランチでdeploy-prodが一覧になく、本番トークンが入るジョブ自体ができないかを確認します。
GitLabは、保護変数を保護ブランチのパイプラインにだけ渡します。その機能がないこの環境では、同じ意図を「本番ジョブはmainでだけ作る」というルールで表現します。どちらも、「誰がこの秘密を持つジョブを実行させられるか」を減らす仕組みです。
マスキングがないとログに何が残るか
ジョブbad-debug(deploy、environmentはproduction、mainでだけ)を追加して、echo "debugging with $DEPLOY_TOKEN"とecho -n "$DEPLOY_TOKEN" | base64を実行させ、コミットしてください。実行したあと、このジョブのログ(.gitlab-ci-local/output/bad-debug.log)を/root/glci-secrets/leak-evidence.logにコピーし、このファイルを.gitignoreに追加してコミットされないようにしてください。
gitlab-ci-localはマスキングをしないので、値がそのまま出力されます。GitLabのマスキングは値とまったく同じ文字列だけを隠すので、base64のように形を変えるとそのまま残ります。ログは保存され、多くの人が見ます。
ログに秘密が出力されていないかを機械に見させる
/root/glci-secrets/leak-scan.sh <저장소>を作ってください。リポジトリを一時的なコピーにしてコミットし、パイプラインを動かしたあとで、.gitlab-ci-local/output/*.logに、プロジェクト変数ファイルのうち名前にTOKEN・PASSWORD・SECRET・KEYが含まれる変数の値(8文字以上、環境別の値を含む)が、そのままかbase64の形で入っていれば、LEAK <잡> <변수>を1行ずつ(ソート済み)出力して3で終了します。なければOKを出力して0で、パイプラインが失敗すればERRORを出力して1で終了します(プレースホルダーは、順にリポジトリ、ジョブ名、変数名です)。元のリポジトリには痕跡を残しません。このリポジトリで実行すると、LEAK bad-debug DEPLOY_TOKENが出力されるはずです。
base64は、値の末尾に改行が付いているかどうかで結果が変わります。両方の形を探してください。コピーには、変数ファイル(コミットしていないファイル)も一緒にコピーされている必要があります。そうしないと、パイプラインが同じ値を受け取れません。
秘密は出力せず、入っているかどうかだけを確認する
bad-debugを削除し、代わりにジョブcheck-token(deploy、environmentはproduction、mainでだけ)を追加して、test -n "$DEPLOY_TOKEN" && echo token-presentだけを実行するようにしてください。コミットしたあと、leak-scan.shがOKを出力するはずです。
デバッグに必要なのは、たいてい値ではなく「入ってきたか」です。存在・長さ・ハッシュの先頭数文字のように、元に戻せない情報だけを残せば、ログを共有しても安全です。set -xも変数の値を展開して出力するので、デプロイジョブでは避けます。
動かせる参照を見つける
/root/glci-secrets/pin-audit.sh <설정파일>を作ってください。includeのprojectの項目のrefが40桁のコミットSHAでない場合、remote includeの場合、あるいはcomponentが@<40자리 SHA>で終わらない場合はUNPINNED include <값>を、ジョブのimageに@sha256:のダイジェストがない場合はUNPINNED image <값>を、1行ずつ(ソート済み、重複なし)出力して3で終了し、なければOKを出力して0で終了します(プレースホルダーは、順に設定ファイル、40桁のSHA、値です)。projectの項目の値は<project>@<ref>と書きます。/root/glci-secrets/pin-sample.ymlにサンプル設定(課題本文の下にあるファイル例と同じ内容)を置き、結果を/root/glci-secrets/pin-report.txtに保存してください。
ブランチ名やタグは人が付け替えられるラベルなので、昨日と今日で同じ設定が違うコードを取ってくることがあります。コミットSHA・イメージのダイジェストは、内容そのもののアドレスなので動かせません。リモートファイル(remote)は固定する方法がないので、リポジトリの中に取り込むほうがよいです。