Tellerでキーストアからシークレット情報取得&ソースコード埋め込みを検知する

| 7 min read
Author: noboru-kudo noboru-kudoの画像

これは、豆蔵デベロッパーサイトアドベントカレンダー2022第7日目の記事です。

昨今セキュリティ意識の高まりとともに、シークレット情報の運用は以前よりも注目度が高くなっていると感じます。
また、DevSecOpsの浸透もあり、ソフトウェアライフサクル全体で継続的にセキュリティを確保することが推奨されています。
このような背景から、シークレット情報をAWS Secrets Manager/AWS Systems Manager Parameter StoreHashiCorp Vault等のキーストア製品を使って管理することが一般的かと思います。

このような製品は便利である一方で、ツール固有のAPIやコマンドライン等、格納されたシークレット情報にアクセスするにはそれぞれの手順に従う必要があります。
また、ある程度の規模のシステムでは、一度ソースコードにシークレット情報を直接埋め込んでしまうと、後から検知するのは難しいことも多いでしょう。

ここでは、このような課題を解決するTellerを試してみます。
Tellerは、CNCFの2022/04にサンドボックスプロジェクトとしてホスティングされ、今後の普及が見込まれる製品です。

Tellerは、各種キーストア製品/サービスに格納されたシークレット情報をソフトウェアライフサイクル全体で安全に管理することを目的としたOSSです。
また、アプリケーションにシークレット情報を提供するだけでなく、ソースコード中に埋め込んでしまったものを検知することも可能です。

対応する製品も、主要なものはほとんどサポートしています。

もちろん複数の製品を組み合わせて使うことも可能です。
バックエンドとなる製品への切り替えが発生しても、アプリケーションを変更せずに段階的に移行するといった使い方もできます。

Tellerをインストールする

#

まずは、TellerのCLIをインストールします。macOSではHomeBrewよりインストール可能です。

brew tap spectralops/tap && brew install teller

Windows/Linuxの場合は、GitHubのリリースページよりバイナリファイルがダウンロード可能です。

teller version
> Teller 1.5.6
> Revision 7b714bc2f1d5e14920f2add828fdf7425148ff6b, date: 2022-10-13T08:02:44Z

ここでは現時点で最新の1.5.6をインストールしました。

パラメータストアのシークレット情報を取得する

#

今回はAWS Systems Manager Parameter Store(以下パラメータストア)に格納したシークレット情報をTellerで管理するようにします。

事前準備として、パラメータストアに以下のようにシークレット情報(/myapp/prod/token)を登録しました。

AWS Management Console - SSM Parameter Store

次に、Tellerの設定ファイルを作成します。
もちろん手動でも作成できますが、teller newコマンドを使うと対話形式で作成できます。

teller new

> ? Project name? sample
> ? Select your secret providers [Use arrows to move, space to select, <right> to all, <left> to none, type to filter]
> [ ] .env
> [ ] 1Password
> > [x] AWS SSM (aka paramstore)
> [ ] AWS Secrets Manager
> [ ] Azure Key Vault
> [ ] Cloudflare Workers K/V
> [ ] Cloudflare Workers Secrets
> [ ] Consul
> [ ] CyberArk Conjure
> [ ] Doppler
> ? Would you like extra confirmation before accessing secrets? No

ここでは、プロバイダーとしてAWS SSM (aka paramstore)のみを選択しましたが、前述の通り複数製品の組み合わせも可能です。

カレントディレクトリに.teller.ymlが作成されます。
作成されたファイルを以下のように修正します。

project: sample

# Set this if you want to carry over parent process' environment variables
# carry_env: true

#
# Variables
#
# Feel free to add options here to be used as a variable throughout
# paths.
#
opts:
stage: development

#
# Providers
#
providers:
# configure only from environment
aws_ssm:
env:
# 以下を修正
MYAPP_TOKEN:
path: /prod/myapp/token
decrypt: true

Tellerは、どんな言語でも使える環境変数が統一インターフェースです。
ここでは、環境変数MYAPP_TOKENにパラメータストアのパス/myapp/prod/tokenの値を設定しました。
また、パラメータストアでセキュア文字列として設定しましたのでdecrypt: trueとして、環境変数設定時に復号化するようにします。

パラメータストアは個別に環境変数を指定する必要がありますが、使用するプロバイダー(キーストア製品)によっては特定のパス配下やファイル全体を一括で取り込むこともできます(この場合はenv_syncを使います)。

設定後はパラメータストアからシークレット情報が取得できるかを確認します。
以下のコマンドを実行します。

teller show

> -*- teller: loaded variables for sample using .teller.yml -*-
>
> [aws_ssm /myapp/prod/token] MYAPP_TOKEN = my*****

Tellerがパラメータストアからシークレット情報を取得できていることが分かります[1]

ここで以下のサンプルアプリケーション(app.js)を用意しました。

console.log(process.env.MYAPP_TOKEN);

環境変数よりシークレット情報を取得して、コンソールに出力するだけです。

Tellerとアプリケーションを連携するには、teller runに続けてプログラム実行コマンドを記述するだけです[2]

teller run node app.js
> -*- teller: loaded variables for sample using .teller.yml -*-
> my-super-secret-token

期待通り標準出力にパラメータストアのシークレット情報が出力されました。
Tellerがアプリケーション実行前にパラメータストアよりシークレット情報を取得して、アプリケーションの実行プロセスの環境変数として設定してくれています。
事前にパラメータストアから取得/環境変数exportしたり、.bashrc/.zshrcを編集する必要はなくシンプルで安全です。

Teller run image

標準出力のシークレット情報出力を抑止する

もちろん実運用するうえで、標準出力にシークレット情報を出力するのはNGです。そうは言ってもデバッグ目的で標準出力に出してしまう人もいるかもしれません。
そんな事態が予想される場合は、実行時にtellerコマンドに--redactオプションをつけると、Tellerがアプリケーション内での標準出力への表示を抑止してくれます。

teller run --redact node app.js
> -*- teller: loaded variables for sample using .teller.yml -*-
> **REDACTED**
サブプロセスにもTellerの環境変数を引き継ぐ

Tellerはデフォルトでは、teller runコマンドで指定したプロセスのみにシークレット情報の環境変数を適用します。
アプリケーション内でサブプロセスを起動する場合は、.teller.ymlでcarry_env: trueを指定します。こうするとTellerはOSレベルで環境変数に設定してくれます。

環境別にシークレット情報の取得元を切り替える

#

先程は、パラメータストアのパスが/myapp/prod/...と商用環境向けを示すものでした。
一般的には、環境別にシークレット情報が異なることがほとんどでしょう。
環境別に.teller.ymlを用意するのは今ひとつですので、テンプレート化して動的に切り替えられるようにしてみます[3]

まず、.teller.ymlのoptsのstageを環境変数から取り込むようにします。

opts:
stage: env:MYAPP_STAGE # <- 環境変数(MYAPP_STAGE)より取得

optsは変数置換が可能なセクションで、キーバリューの形式で記述できます。
キーは任意ですので、stageでなくても構いません。
また、値にenv:XXXXXのフォーマットで記述し、tellerコマンド実行時に環境変数より取得するようにします。

パラメータストアのシークレット情報のパスは以下のようになります。

providers:
# configure only from environment
aws_ssm:
env:
MYAPP_TOKEN:
path: /myapp/{{stage}}/token # stageにより動的にパス切り替え
decrypt: true

path部分を/myapp/{{stage}}/tokenとして先程可変としたstageから取り込むようにしました。

実行コマンドは以下のようになります。

# パラメータストアパス:/myapp/prod/token
MYAPP_STAGE=prod teller run node app.js

# パラメータストアパス:/myapp/dev/token
MYAPP_STAGE=dev teller run node app.js

環境(MYAPP_STAGE)はシークレット情報ではありませんので、.bashrc/.zshrc等で環境別に設定しておいても良いかと思います。

ソースコードへのシークレット情報埋め込みを検知する

#

ここまでパラメータストアからシークレット情報を取得して、アプリケーションから参照するところを見てきましたが、Tellerにはソースコード内にシークレット情報がハードコードされているかをチェックする機能もあります。

例えば、以下のソースコードを実装してしまったとします。

const token = "my-super-secret-token"
console.log(token);

これをチェックするにはteller scanコマンドを実行します。

MYAPP_STAGE=prod teller scan
> [high] app.js (2,15): found match for aws_ssm/MYAPP_TOKEN (my*****)
>
> Scanning for 1 entries: found 1 matches in 6.301915ms
echo $?
> 1

Tellerはファイル全体に対してスキャンを行い、.teller.ymlで指定したシークレット情報を直接埋め込んでいる部分を検知します。
ここでは、重要度highとしてapp.jsにシークレット情報埋め込みを検知していることが分かります。
重要度は.teller.ymlのseverityでシークレット情報ごとに指定できます(デフォルトはhigh)。

providers:
# configure only from environment
aws_ssm:
env:
MYAPP_TOKEN:
path: /myapp/{{stage}}/token
decrypt: true
severity: low # high | medium | low | none

重要度がhighまたはmiddleで検知された場合は、teller scanはExitコード1を返します。
CI/CDパイプラインにこの脆弱性スキャンを取り込むことで、シークレット情報の埋め込みを検知し、パイプラインを失敗させるといった使い方が想像できますね。

最後に

#

ここでは紹介できませんでしが、Tellerには他にもシークレット情報のプロバイダー間のドリフト検知、同期や更新・削除といった機能もあります。

Tellerという統一インターフェースでバックエンドのキーストアを意識せずに、安全に利用できるのは大きなメリットと言えそうです。
機会があれば取り入れてみたいなと思いました。


  1. 取得できない場合は、実行しているIAMポリシーを確認してください。Tellerがローカル環境のAWSクライアント設定でパラメータストアを参照するので、IAMユーザー・ロールで該当のパラメータストア参照可能である必要があります。 ↩︎

  2. アプリケーションの実行にスイッチが必要な場合は、teller run -- myapp --switch fooのようにrunの後ろに--を入れます。 ↩︎

  3. Tellerコマンドで-cオプションをつけることでファイルの切り替えは可能です。 ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  2. Docker+Wasm で WASM をコンテナとして実行する (2023-01-25)
  3. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  4. 直感が理性に大反抗!「モンティ・ホール問題」 (2022-07-04)
  5. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  6. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  7. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  8. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)