TT Lab
はじめる
学ぶ 学習パス コース

GitLab CI/CD

マスキングを信じていたのに base64 でトークンがログに残った

TT Labで続きを見る

目標

変数が入ってくる場所の優先順位と環境スコープを実行で確認し、本番の秘密が入るジョブをmainに絞ったあとで、マスキングのないログに秘密が残る様子を見て、ログ漏えい検査器と参照固定検査器を作ります。

なぜ重要なのか

パイプラインは、リポジトリで最も強い権限を持って任意のコードを実行する場所です。同じ名前の変数が複数の場所から入ってくるとき、どの値が勝つかを知っておかないと、本番の値が上書きされる事故を防げず、環境スコープとブランチのルールで、秘密が入るジョブの数を減らす必要があります。マスキングは値とまったく同じ文字列だけを隠すので、形を変えた出力はそのまま残り、includeとイメージをラベルで参照すると、誰かがそのラベルを付け替える日に、自分たちのパイプラインが他人のコードを実行します。

ステップ

  1. /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になるはずです。
  2. ジョブを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)を出力するはずです。
  3. deploy-prodにrules: - if: $CI_COMMIT_BRANCH == "main"を追加して、コミットしてください。採点ツールは、コピーのfeatureブランチでdeploy-prodが一覧になく、本番トークンが入るジョブ自体ができないかを確認します。
  4. ジョブ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に追加してコミットされないようにしてください。
  5. /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が出力されるはずです。
  6. bad-debugを削除し、代わりにジョブcheck-token(deploy、environmentはproduction、mainでだけ)を追加して、test -n "$DEPLOY_TOKEN" && echo token-presentだけを実行するようにしてください。コミットしたあと、leak-scan.shがOKを出力するはずです。
  7. /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に保存してください。

参考

同じ名前の変数が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)は固定する方法がないので、リポジトリの中に取り込むほうがよいです。