バックエンドエンジニアの星野です。 東京オリンピック2020が開幕されましたね。LCLは移転前のオフィスがオリンピック選手村に近い勝どきにあったので選手に親近感を覚えます。
さて、LCLではバックエンド開発のWebアプリケーションフレームワークとしてRuby on Railsを採用しています。 先日、Railsのアップデートや構成の更新をした際にCredential機能について見直す機会があったのでその知見を共有したいと思います。
tl;dr
master.key
はGitリポジトリに含めない- マルチステージビルドか--mount=type=secretを活用してDockerイメージにも
master.key
を含めない - Amazon ECSではSSMパラメータのSecureStringを利用して環境変数
RAILS_MASTER_KEY
を設定する
Credential機能の概要
Railsはバージョン5.2からCredential機能としてsecret_key_base
などの秘匿情報を扱うファイルがcredentials.yml.enc
に変わり、それ以前のsecrets.yml
は非推奨となりました。*1
Credential機能はヘルプコマンドを実行すると表示されるヘルプを読むのが一番良いのですが軽くおさらいします。
$ bin/rails credentials:help
credentials.yml.enc
は5.2以上でrails newした時やファイルが存在しない場合にbin/rails credentials:edit
を実行すると作成されます。
このファイルは暗号化されておりそのままエディターで編集することはできません。
$ cat config/credentials.yml.enc PaHIdEFLuFWnL3uNFhpV0ck4vkx8mXpClQuvTfeXL+Dgdbdi7AkOZuEl5Wq6I+AbOcRj4HzEImZgU9Fs1IX5FfISab3/oyZnSfWrQsPDWZBMaTlgbB3lbbepKBvnKIvvRpbFP2gPxqy02NbcrgLv5MoEWXky+Ku7vmSz0dr6vxMo5X3n0k5BRVRKkOKglD9r2sFqbcOC5QY13PxhSkgU1r3OZk//sDtHYTwZxErNqNwZhH+teHS/PFSHc+Uniz6OnHGUSqHela3gKVZ0KN62gLtKpGd5PC9AocGNagRHuN6AweddERBUX/jE1eBP9fNrSZEt23dSceXH6q0tERgYs4G81FX6isi8xQeOBlw43iAXjJboVN3gjdT3rfgNFU4MXf0Ae7PdYApTc5M3RSgYvhlD/Sg+lmSR1wAG--8YbTFnxQDeAiCvc1--mFGkxKAUuYXj23V3VZMu6g==
ここでこの記事での主役となるmaster keyが登場します。初期状態ではcredentials.yml.enc
と同じconfigにあるファイルmaster.key
の値が復号に必要です。
そのmaster.key
が存在するか環境変数RAILS_MASTER_KEY
の値としてセットされている状態でeditを実行するとcredentials.yml.enc
が編集できます。
$ bin/rails credentials:edit # 環境変数$EDITORが未設定の場合 $ EDITOR=vi bin/rails credentials:edit
master.key
はその特性上リポジトリに含めてはいけないので.gitignore
ファイルで除外することが必須です。
別の安全な場所に値を保管しましょう。
ファイルの中身は次のようになっておりsecret_key_base
など外部に公開してはいけない秘匿情報を書き込みます。*2
# aws: # access_key_id: 123 # secret_access_key: 345 # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: abcde12345
前述のmaster.key
かRAILS_MASTER_KEY
が設定されていれば、この値はアプリからは透過的に取得可能です。
irb(main):001:0> Rails.application.credentials => #<ActiveSupport::EncryptedConfiguration:0x00007fd370954570 @content_path=#<Pathname://credentials_sample/config/credentials.yml.enc>, @env_key="RAILS_MASTER_KEY", @key_path=#<Pathname:/credentials_sample/config/master.key>, @raise_if_missing_key=false> irb(main):002:0> Rails.application.credentials.secret_key_base => "abcde12345"
他にも次のように環境を指定することで環境別にCredentialを作成できたり、
bin/rails credentials:edit --environment development
config.credentials.content_path
とconfig.credentials.key_path
の指定でディレクトリを変更する機能もあるので詳細はhelpコマンドから確認してください。
本番環境向けDockerイメージでのmaster keyの取り扱い方
前置きが長くなりましたがここから本題に入ります。
LCLでは本番環境向けのソフトウェアデリバリーにDockerを採用しています。
The Twelve-Factor AppやDockerのセキュリティプラクティスに従い、master keyを含む秘匿情報がビルド時にDockerイメージに含まれないようにして、イメージの起動時に環境変数RAILS_MASTER_KEY
を設定します。
master keyをDockerイメージに含まないことによる課題
Dockerビルド時のRailsアセットプリコンパイルはRAILS_ENV=production
で実行するためmaster keyが必要になりますが、CI/CDのクリーンな環境で実行する場合、リポジトリにはmaster.key
を含めないためビルドに失敗してしまいます。*3
また、ローカル環境からビルドする場合もmaster.key
を.dockerignore
に追加してビルドコンテキストに含めるべきではありません。
そのためビルド時だけ一時的にmaster keyを渡してビルドしたDockerイメージには含めない工夫が必要になります。
🙅♀️ARG+ENVを使う方法(NGな方法)
FROM ruby:3.0.2 ARG master_key ENV RAILS_MASTER_KEY=master_key 中略 RUN rails assets:precompile RAILS_ENV=production
docker build -t rails-app:production --build-arg master_key=foo .
ARGを使うことでビルド時の引数としてmaster keyを渡しENVをセットすることでDockerfileには書き込まずにビルドが実行できます。 master_key=fooは例えばGitHub ActionsのRepository Secretのように秘匿情報の安全な保管先から読み込みます。
この方法には問題があり、docker historyコマンドを実行するとRAILS_MASTER_KEY
の値が参照できてしまうので利用するべきではありません。
🙆♀️マルチステージビルドを使う方法
FROM ruby:3.0.2 AS builder ARG master_key ENV RAILS_MASTER_KEY=master_key 中略 RUN rails assets:precompile RAILS_ENV=production FROM ruby:3.0.2 AS release 中略 COPY --from=builder public/assets public/
docker build -t rails-app:production --build-arg master_key=foo --target release .
先ほどとあまり変わらないように見えますが、Dockerのマルチステージビルドを利用してビルドします。
RAILS_MASTER_KEY
を設定したbuilderはビルドのみに利用することで最終的な成果物であるreleaseイメージにはENVが残りません。
マルチステージビルドの詳細については公式のドキュメントを参照してください。
🙆♂️--mount=type=secretを使う方法
FROM ruby:3.0 中略 RUN --mount=type=secret,id=master_key,target=master.key,required=true rails assets:precompile RAILS_ENV=production
$ docker buildx build \ --secret id=master_key,src=master.key \ -t mount-type-secret:test .
この例ではDockerfileの--mount=type=secretとbuildkit(buildx)の--secretオプションを使い、指定したファイルを一時的にマウントすることで秘匿情報を残さずビルドしています。
--mount=type=secretで指定したtargetは--secretの引数でmaster.key
のファイルパスを指定します。
例えばGitHub Actionsでビルドする場合は事前にファイルとして書き出しておきます。
- name: Create rails master key for build run: | touch $GITHUB_WORKSPACE/master.key echo ${{ secrets.RAILS_MASTER_KEY }} > $GITHUB_WORKSPACE/master.key
--secretのオプションを使うためにはdocker buildx buildコマンドかDOCKER_BUILDKIT=1
を設定してdocker buildするかbuildkit直でビルドのいずれかが必要になるので、
Dockerデーモンのバージョンが古いが更新できないなど環境によっては利用できないため注意が必要です。その場合はマルチステージビルドを使いましょう。
DockerfileがシンプルになるのでLCLではこの方法を採用しています。*4
シンタックスの詳細はドキュメントを参照してください。
Amazon ECSでmaster keyを読み込む
最後はDocker起動編です。
LCLではDockerの実行環境としてAmazon ECS(Fargate)を採用しています。*5
ECSではタスク定義に環境変数を設定できますが、master keyは秘匿情報なためSSMパラメータストアのSecureStringを利用して設定します。
タスク定義の抜粋
"secrets": [ { "name": "RAILS_MASTER_KEY", "valueFrom": "arn:aws:ssm:ap-northeast-1:012345678901:parameter/awesome_app/master_key" } ]
まとめ
これでmaster keyをDockerイメージに含めることなくビルドして実行することができました。
実際はGitリポジトリ(GitHub)もDockerレジストリ(Amazon ECR)もプライベートなので第三者がmaster keyを閲覧できる可能性は低いのですが、記憶に新しいCodecovの改ざんなど用心するに越したことはありません。
Railsはメニューはおまかせなのでデフォルトでいい感じに設定してくれますが抜け目なく設定しようとすると、Credential一つとっても考慮すべきことが多いと感じました。
参考記事
採用情報
LCLでは開発メンバーを募集しております!
もし興味をお持ちになりましたらお気軽に応募してください。
*1:5.1には別の仕組みがありましたがすでにサポートが切れているため割愛します。
*2:AWSの認証情報は例えば実行環境がAmazon EC2の場合EC2インスタンスのインスタンスプロファイルにアタッチしたIAMロールをAWS SDKがインスタンスメタデータ経由で自動的にに取得してくれるので特別な理由がない限りはここに書かないことを推奨します。HerokuからS3バケットを参照したいような時は有効かもしれません。
*3:RailsをAPIモードで利用していてアセットプリコンパイルしない場合はRailsのディレクトリをコピーするだけなので問題になりません。
*4:以前は実験的機能だったのでDockerfileにexperimentalのフラグを書く必要があり、記事執筆現在ネットの記事ではその前提が多いですが、buildkit v0.8.0からmainlineに取り込まれたので(Dockerfile frontend1.2.0)安定版の機能として採用できます。
*5:Fargate PV 1.4.0からコンテナラインタイムがcontainerdに変更されたのでDockerの実行環境という説明が正しいか自信がないです。