こんにちは osyoyu です。最近は環境変数の扱いの整理に取り組んでいました。
ところで、秘匿値を環境変数で管理する場合、改行文字(0x0A = LF)を含む環境変数を取り扱わねばならないケースは稀にあります。もっともありがちな例は秘密鍵のPEMでしょうか。GitHub Appを作成するとよくあるケースですね。
-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQCw0YNSqI9T1VFvRsIOejZ9feiKz1SgGfbe9Xq5tEzt2yJCsbyg +xtcuCswNhdqY5A1ZN7G60HbL4/Hh/TlLhFJ4zNHVylz9mDDx3yp4IIcK2lb566d fTD0B5EQ9Iqub4twLUdLKQCBfyhmJJvsEqKxm4J4QWgI+Brh/Pm3d4piPwIDAQAB AoGASC6fj6TkLfMNdYHLQqG9kOlPfys4fstarpZD7X+fUBJ/H/7y5DzeZLGCYAIU +QeAHWv6TfZIQjReW7Qy00RFJdgwFlTFRCsKXhG5x+IB+jL0Grr08KbgPPDgy4Jm xirRHZVtU8lGbkiZX+omDIU28EHLNWL6rFEcTWao/tERspECQQDp2G5Nw0qYWn7H Wm9Up1zkUTnkUkCzhqtxHbeRvNmHGKE7ryGMJEk2RmgHVstQpsvuFY4lIUSZEjAc DUFJERhFAkEAwZH6O1ULORp8sHKDdidyleYcZU8L7y9Y3OXJYqELfddfBgFUZeVQ duRmJj7ryu0g0uurOTE+i8VnMg/ostxiswJBAOc64Dd8uLJWKa6uug+XPr91oi0n OFtM+xHrNK2jc+WmcSg3UJDnAI3uqMc5B+pERLq0Dc6hStehqHjUko3RnZECQEGZ eRYWciE+Cre5dzfZkomeXE0xBrhecV0bOq6EKWLSVE+yr6mAl05ThRK9DCfPSOpy F6rgN3QiyCA9J/1FluUCQQC5nX+PTU1FXx+6Ri2ZCi6EjEKMHr7gHcABhMinZYOt N59pra9UdVQw9jxCU9G7eMyb0jJkNACAuEwakX3gi27b -----END RSA PRIVATE KEY-----
こういう文字列を環境変数に入れたいことがある、ということです。ちなみにこれはRFC 9500の例示用の鍵です。
さて、これを実践しようとすると意外にも苦労します。改行文字を適当に \n に変換して入れてみると期待通り動かなかったり、では \\n にしてみてやはり動かなかったり。しかし秘匿すべき値なのでログに出してデバッグしづらかったり、そもそもデバッグ用にbashから渡すのが難しかったり、と散々です。
結論:改行文字からは逃げ回るのがラク
POSIXでは8.1 Environment Variable Definitionで「環境変数」を “The value of an environment variable is an arbitrary sequence of bytes, except for the null byte.” (ヌル文字を含まない任意のバイト列)と明確に定義していますが、実際にシステムをまたいで「環境変数」を受け渡すときにはこれより強い制約が課されることが常でしょう。
改行文字は殊更に取り扱いが一貫しない文字です。身もふたもない結論ですが、環境変数の値の全体をbase64するなどして改行文字を含まない形に変換した上で、その値を実際に利用するプロセスに到着してから元の文字列に戻すのが最も安定します。
その上で、それでも base64 エンコーディングしたくないこともあります。各種システムでの改行文字の取り扱いの様子を見ていきましょう。
以下では「改行文字」という表現はすべて 0x32 (LF) の1バイトを指します。一方、\n という表現はすべて \ n の2バイトの文字列を指します。
bash
コンテナのエントリポイントでよく使われ、環境変数のexportも担いがちなbashですが、改行文字の取り扱いは少々厄介です。
- 文字列リテラル中の
\nは改行文字にならない - bashの組み込みのechoは文字列中のエスケープシーケンスを解釈しない
echo $SOME_OUTPUT | grep foo のようなコードは改行文字が含まれる場合、期待通りに動作しません。 "$SOME_OUTPUT" のようにquoteするのがよいでしょう。
# これは3バイトの文字列: a LF b $ export VAR1="a b" $ echo $VAR1 # 改行文字で複数の引数に展開され echo a b と等価になる a b $ echo "$VAR1" # quote すれば1引数の呼び出しになるので $VAR1 中の改行文字が維持される a b # これは4バイトの文字列: a \ n b $ export VAR2="a\nb" $ echo $VAR2 # bash 組み込みの echo は \n を解釈しない a\nb $ echo -e $VAR2 # echo -e なら解釈する a b # これは3バイトの文字列: a LF b (VAR1 と同じ) $ export VAR3=$'a\nb'
shでは echo -e が使えないこと以外は同じです。ここでの “sh” はbashのPOSIX modeおよびdashのことです。
zsh
bashとは挙動が異なります。デフォルトで変数が複数の引数に展開されないため、bashより少し扱いやすくなっています(setopt SH_WORD_SPLIT でsh/bash互換の挙動になります)。
また、echoがエスケープシーケンス \n も解釈します。しかしやはり文字列リテラル中の \n は改行文字になりません。
# これは3バイトの文字列: a LF b % export VAR1="a b" % echo $MY_VAR_1 # 変数を quote しなくても1引数になるので改行文字もそのままprintされる a b # これはやはり4バイトの文字列: a \ n b % export VAR2="a\nb" % echo $VAR2 # zsh の echo は \n を解釈するが a b % ruby -e 'print ENV["VAR2"]' # $VAR2 自体に改行文字は含まれているわけではない a\nb
macOSに合わせたスクリプティングではzshを使うこともあるかもしれません。そうでなくとも、デバッグなどで手元で export SECRET_KEY="..." rails s などをする際に覚えておくと便利かもしれません。
JSON
環境変数をJSONで管理するケースもよく見られます。特に秘匿値ではないものについてありがちでしょう。
JSONでは文字列に改行文字 (LF) を含むことは許容されていません。代わりにエスケープシーケンス \n を使用することができます。つまり、以下のJSON内の “secret_key” は改行文字を含む文字列です。
{"secret_key": "-----BEGIN PRIVATE KEY-----\n1234567790\n-----END PRIVATE KEY-----"}
RFC 4627, RFC 7159, RFC 8259, ECMA 404のいずれでも \n は U+000A に展開されると定められています。扱いやすいですね。
JSON5, JSONC, Jsonnet
JSONの拡張仕様であるJSON5でも改行文字を文字列中に含むことはできません。JSONCには仕様がないのでなんとも言えませんが、市井のパーサーではJSONと同様の状況のようです。Jsonnetも同様です。
YAML
Kubernetesを扱っているとYAMLで環境変数を定義することがよくあります。なお当社にはKubernetesはありません。
ところでYAMLには「改行文字を含む文字列の記法が6つ、いや9つ…… 厳密には63つある」そうで、非常に大変な言語であることが分かります。いやはや本当に。
それでもあえてYAMLで環境変数を定義するならば、 >- syntax を使うのが安全そうです。 > や | では終わりにも改行文字が入ります。それが期待する挙動ならばかまわないですが。
key1: >- foo bar baz key2: | foo bar baz ↓ 以下のように評価される { "key1": "foo\nbar\nbaz", "key2": "foo\nbar\nbaz\n" } # 終わりにも \n が入ってしまう
ERB として評価される YAML
Railsのエコシステムでは、設定ファイルのYAMLの中でERBを使って環境変数を展開する方式がよく見られます。
config: secret_key: <%= ENV['MY_SECRET_KEY'] %>
ここで <%= ENV['MY_PEM'] %> が改行文字を含む文字列に展開されると悲しいことになります。
config: secret_key: -----BEGIN PRIVATE KEY----- 1234567890 -----END PRIVATE KEY-----
余談ですが、改行文字関係なく、ERB で展開される部分は必ず "<%= ... %>" のようにクォートするのがよいでしょう。YAML の数々のびっくり仕様にハマるよりは、あとから必要に応じて .to_i したほうが幸せというものです。
some_end_date: 2025-03-07 # Date 型の値になる tel_prefix: 045 # 37 になる (8進数)
.env ファイル
.env ファイルを扱えるツールはあまたありますが、その文法は無法地帯です。パーサーの気分次第でしょう。その上で、いくつかのツールで以下の .env ファイルをパースした結果をお知らせします。
ESCAPED="123\n456" BREAKED="123 456"
dotenv gem (Ruby)
{"ESCAPED" => "123\\n456", "BREAKED" => "123\n456"}
"\n"は \ n の2バイトとして扱われる- 改行文字は改行文字として扱われる
dotenv (npm module)
{ ESCAPED: '123\n456', BREAKED: '123\n456' }
"\n"は改行文字として扱われる- 改行文字も改行文字として扱われる
mise-en-place
"\n"は改行文字として扱われる- 改行文字も改行文字として扱われる
AWS Systems Manager Parameter Store
ECSを使っている場合、秘匿すべき環境変数の管理にはAWS Systems ManagerのParameter Storeを使うと便利です。SecureStringとして暗号化された状態で保存できますし、ECSのtask definitionからそのまま参照できます。
Parameterには改行文字を含むことができます。特別なことは何もなく、PutParameter APIを呼び出すときのJSONで \n を含む文字列を送ってあげればいいだけです。
AWS CLI aws ssm put-parameter を使う場合は --cli-input-json を使うとよいです。
まとめ:環境変数が通るパーサーはなるべく減らそう
このように、環境変数を取り扱いうる各種機能における改行文字の取り扱いは実にバラバラです。そんな中、\n と入力したとき、それがどこで何度パースされるのか把握するのは難しいです。設定時においても対話的なシェルやREPLで \n を入力するのはなるべく避け、仕様が安定した形式のファイルに書いてからそれをパースすることで勝利に近づけるでしょう。