LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログ

Railsのcredentials.yml.encとmaster keyをDockerで安全に扱う

バックエンドエンジニアの星野です。 東京オリンピック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.keyRAILS_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_pathconfig.credentials.key_pathの指定でディレクトリを変更する機能もあるので詳細はhelpコマンドから確認してください。

本番環境向けDockerイメージでのmaster keyの取り扱い方

前置きが長くなりましたがここから本題に入ります。

LCLでは本番環境向けのソフトウェアデリバリーにDockerを採用しています。 The Twelve-Factor AppDockerのセキュリティプラクティスに従い、master keyを含む秘匿情報がビルド時にDockerイメージに含まれないようにして、イメージの起動時に環境変数RAILS_MASTER_KEYを設定します。

techblog.lclco.com

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が残りません。 マルチステージビルドの詳細については公式のドキュメントを参照してください。

docs.docker.com

🙆‍♂️--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

シンタックスの詳細はドキュメントを参照してください。

docs.docker.com

github.com

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一つとっても考慮すべきことが多いと感じました。

参考記事

qiita.com

terashim.com

採用情報

LCLでは開発メンバーを募集しております!

もし興味をお持ちになりましたらお気軽に応募してください。

www.lclco.com

*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の実行環境という説明が正しいか自信がないです。