ブログ | NGINX

NGINX チュートリアル: コンテナ内のシークレットを安全に管理する方法

NGINX-F5 水平黒タイプ RGB の一部
ロバート・ヘインズ サムネイル
ロバート・ヘインズ
2023年3月14日公開

この投稿は、 2023 年 3 月の Microservices の概念を実践するのに役立つ 4 つのチュートリアルの 1 つです。 マイクロサービスの提供を開始する:

多くのマイクロサービスでは、安全に動作するためにシークレットが必要です。 シークレットの例としては、SSL/TLS 証明書の秘密キー、別のサービスへの認証に使用する API キー、リモート ログインに使用する SSH キーなどが挙げられます。 適切な秘密管理には、秘密が使用されるコンテキストを必要な場所のみに厳密に制限し、必要な場合を除いて秘密にアクセスできないようにすることが求められます。 しかし、アプリケーション開発の急ぎの中で、この作業は省略されることがよくあります。 結果? 不適切な秘密管理は、情報漏洩や不正利用の一般的な原因です。

チュートリアルの概要

このチュートリアルでは、クライアント コンテナーがサービスにアクセスするために使用する JSON Web Token (JWT) を安全に配布して使用する方法を示します。 このチュートリアルの 4 つの課題では、シークレットを管理するための 4 つの異なる方法を試し、コンテナー内でシークレットを正しく管理する方法だけでなく、不適切な方法についても学習します。

このチュートリアルでは JWT をサンプルシークレットとして使用していますが、データベース認証情報、SSL 秘密キー、その他の API キーなど、秘密にしておく必要があるコンテナーのあらゆるものにこのテクニックが適用されます。

このチュートリアルでは、次の 2 つの主要なソフトウェア コンポーネントを活用します。

  • API サーバー– NGINX オープンソースと、JWT からクレームを抽出し、クレームの 1 つから値を返す基本的な NGINX JavaScript コードを実行するコンテナー。クレームが存在しない場合はエラー メッセージを返します。
  • APIクライアント– APIサーバーにGETリクエストを送信するだけの非常にシンプルなPythonコードを実行するコンテナ

チュートリアルの実際のデモについては、このビデオをご覧ください。

このチュートリアルを実行する最も簡単な方法は、 Microservices Marchに登録し、提供されているブラウザベースのラボを使用することです。 この投稿では、独自の環境でチュートリアルを実行する手順を説明します。

前提条件とセットアップ

前提条件

独自の環境でチュートリアルを完了するには、次のものが必要です。

  • Linux/Unix互換環境
  • Linux コマンドラインの基本的な知識
  • nanovimのようなテキストエディタ
  • Docker ( Docker ComposeおよびDocker Engine Swarmを含む)
  • curl (ほとんどのシステムに既にインストールされています)
  • git (ほとんどのシステムに既にインストールされています)

注:

  • このチュートリアルでは、ポート 80 でリッスンするテスト サーバーを使用します。 すでにポート 80 を使用している場合は、 docker runコマンドでテスト サーバーを起動するときに、 ‑pフラグを使用してテスト サーバーに別の値を設定します。 次に、 :<ポート番号> 接尾辞オン ローカルホストカール コマンド。
  • チュートリアル全体を通して、コマンドを切り取ってターミナルに貼り付けやすくするために、Linux コマンドラインのプロンプトは省略されています。 チルダ ( ~ ) はホームディレクトリを表します。

設定

このセクションでは、チュートリアル リポジトリをクローンし認証サーバーを起動し、トークンありとトークンなしでテスト要求を送信します

チュートリアルリポジトリをクローンする

  1. ホーム ディレクトリにmicroservices-marchディレクトリを作成し、そこに GitHub リポジトリをクローンします。 (別のディレクトリ名を使用して、それに応じて手順を変更することもできます。) リポジトリには、シークレットを取得するためにさまざまな方法を使用する構成ファイルと API クライアント アプリケーションの個別のバージョンが含まれています。

    mkdir ~/microservices-marchcd ~/microservices-march
    git クローン https://github.com/microservices-march/auth.git
  2. 秘密を表示します。 これは署名された JWT であり、通常、API クライアントをサーバーに認証するために使用されます。

    cat ~/microservices-march/auth/apiclient/token1.jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

このトークンを認証に使用する方法はいくつかありますが、このチュートリアルでは、API クライアント アプリがOAuth 2.0 Bearer Token Authorization フレームワークを使用してトークンを認証サーバーに渡します。 これには、JWT の前にAuthorization を付ける作業が含まれます。 持ち主 次の例のように:

「承認: ベアラー eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

認証サーバーの構築と起動

  1. 認証サーバーのディレクトリに変更します。

    cd APIサーバ
  2. 認証サーバーの Docker イメージをビルドします (最後のピリオドに注意してください)。

    docker build -t apiserver を実行します。
  3. 認証サーバーを起動し、実行されていることを確認します (読みやすくするために出力は複数行に分かれています)。

    docker run -d -p 80:80 apiserver docker psコンテナ ID イメージ コマンド...
    2b001f77c5cb apiserver "nginx -g 'daemon of..." ... ... 作成ステータス... ... 26秒前 26秒前へ... ポート ... ... 0.0.0.0:80->80/tcp、:::80->80/tcp、443/tcp ... ... 名前 ...relaxed_proskuriakova

認証サーバーをテストする

  1. 認証サーバーがJWTを含まないリクエストを拒否し、401必要な承認:

    curl -X GET http://localhost<html>
    <head><title>401 認証が必要です</title></head>
    <body>
    <center><h1>401 認証が必要です</h1></center>
    <hr><center>nginx/1.23.3</center>
    </body>
    </html>
  2. Authorizationヘッダーを使用して JWT を提供します。 の200OK戻りコードは、API クライアント アプリが正常に認証されたことを示します。

    curl -i -X GET -H "認証: ベアラー `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK サーバー: nginx/1.23.2 日付: DD 月 YYYY hh : mm : ss TZコンテンツタイプ: text/html コンテンツの長さ: 64 最終更新日: DD 月 YYYY hh : mm : ss TZ接続: キープアライブ ETag: 「63dc0fcd-40」Xメッセージ: 成功 apiKey1 Accept-Ranges: バイト { "response": "success", "authorized": true, "value": "999" }

課題1: アプリに秘密をハードコードする (そうではありません!)

このチャレンジを始める前に、はっきりさせておきたいことがあります。アプリに秘密をハードコーディングするのは、最悪のアイデアです。 コンテナ イメージにアクセスできるユーザーなら誰でも、ハードコードされた資格情報を簡単に見つけて抽出できることがわかります。

このチャレンジでは、API クライアント アプリのコードをビルド ディレクトリにコピーしアプリをビルドして実行しシークレットを抽出します

APIクライアントアプリをコピーする

apiclientディレクトリのapp_versionsサブディレクトリには、4 つのチャレンジに対応するシンプルな API クライアント アプリのさまざまなバージョンが含まれており、それぞれが前のバージョンよりもわずかに安全になっています (詳細については、 「チュートリアルの概要」を参照してください)。

  1. API クライアント ディレクトリに変更します。

    cd ~/microservices-march/auth/apiclient
  2. このチャレンジのアプリ(ハードコードされたシークレットを持つアプリ)を作業ディレクトリにコピーします。

    cp ./app_versions/very_bad_hard_code.py ./app.py
  3. アプリを見てみましょう:

    cat app.py import urllib.request import urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Bearer " + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("Authorization", authstring) を試してください: urllib.request.urlopen(req) をレスポンスとして実行します: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message) ただし、urllib.error.URLError は e として実行します: print(str(e.code) + " s " + e.msg)

    コードは単にローカル ホストに要求を送信し、成功メッセージまたは失敗コードのいずれかを出力します。

    リクエストでは、次の行にAuthorizationヘッダーが追加されます。

    req.add_header("認証", authstring)

    他に何か気づきましたか? おそらくハードコードされた JWT でしょうか? それについてはすぐに説明します。 まずアプリをビルドして実行してみましょう。

APIクライアントアプリをビルドして実行する

docker composeコマンドを Docker Compose YAML ファイルとともに使用しています。これにより、何が起こっているのか理解しやすくなります。

(前のセクションのステップ 2 で、チャレンジ 1 に固有の API クライアント アプリの Python ファイル ( very_bad_hard_code.py ) の名前をapp.pyに変更したことに注意してください。 他の 3 つのチャレンジでも同じことを行います。 毎回app.py を使用すると、 Dockerfile を変更する必要がないため、ロジスティクスが簡素化されます。 つまり、毎回コンテナの再構築を強制するには、 docker composeコマンドに‑build引数を含める必要があります。

docker composeコマンドは、コンテナを構築し、アプリケーションを起動し、単一の API 要求を行い、コンテナをシャットダウンし、API 呼び出しの結果をコンソールに表示します。

200出力の最後から 2番目の行にある成功コードは、認証が成功したことを示します。 apiKey1 の値は、認証サーバーが JWT 内のその名前のクレームをデコードできたことを示しているため、さらに確認になります。

docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 成功 apiKey1 apiclient-apiclient-1 はコード 0 で終了しました

したがって、ハードコードされた資格情報は、API クライアント アプリでは正しく機能しました。これは驚くことではありません。 しかし、それは安全でしょうか? おそらくそうでしょう。コンテナは終了する前にこのスクリプトを 1 回だけ実行し、シェルも持っていないからです。

実際のところ、まったく安全ではありません。

コンテナイメージからシークレットを取得する

資格情報をハードコーディングすると、コンテナのファイルシステムの抽出は簡単な作業であるため、コンテナイメージにアクセスできるすべてのユーザーが資格情報を検査できるようになります。

  1. 抽出ディレクトリを作成し、そこに移動します。

    mkdir extractcd 抽出
  2. コンテナ イメージに関する基本情報を一覧表示します。 --formatフラグにより、出力がより読みやすくなります (同じ理由で、ここでも出力は 2 行に分かれています)。

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"コンテナ ID 名前 イメージ ...
    11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... 作成ステータス... 6 分前 終了 (0) 4 分前 ... 43分前 43分前
  3. 最新のapiclientイメージを.tarファイルとして抽出します。 のために <コンテナID>の値を代入します 容器 ID 上記の出力のフィールド(11b73106fdf8 このチュートリアルでは:

    docker export -o api.tar <コンテナID>

    コンテナのファイル システム全体を含むapi.tarアーカイブを作成するには、数秒かかります。 秘密を見つける 1 つの方法は、アーカイブ全体を抽出して解析することですが、興味深いものを見つけるためのショートカットがあることが分かりました。それは、 docker historyコマンドを使用してコンテナーの履歴を表示することです。 (このショートカットは、Docker Hub または別のコンテナー レジストリにあるコンテナーにも機能し、 Dockerfileがなくコンテナー イメージのみがある可能性があるため、特に便利です)。

  4. コンテナの履歴を表示します。

    docker history apiclientイメージが作成されました...
    9396dde2aad0 8 分前 ... <欠落> 8 分前 ... <欠落> 28 分前 ... ... サイズによって作成... ... CMD ["python" "./app.py"] 622B ... ... コピー ./app.py ./app.py # buildkit 0B ... ... WORKDIR /usr/app/src 0B ... ... コメント ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0

    出力行は逆時系列順になります。 作業ディレクトリが/usr/app/srcに設定され、アプリの Python コードのファイルがコピーされて実行されたことがわかります。 このコンテナのコア コードベースが/usr/app/src/app.pyにあることは、優れた探偵でなくても推測できます。そのため、資格情報の保存場所としてはそこが適切です。

  5. その知識を武器に、そのファイルだけを抽出します。

    tar --extract --file=api.tar usr/app/src/app.py
  6. ファイルの内容を表示すると、それだけで「安全な」JWT にアクセスできるようになります。

    usr/app/src/app.py に cat します... jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" ...

チャレンジ2: シークレットを環境変数として渡す (繰り返しますが、ダメです!)

2023 年 3 月のマイクロサービスのユニット 1 (Twelve-Factor App をマイクロサービス アーキテクチャに適用する) を完了している場合は、環境変数を使用して構成データをコンテナーに渡す方法に精通していることになります。 見逃してしまった場合でもご心配なく。登録後、オンデマンドで視聴できます。

このチャレンジでは、シークレットを環境変数として渡します。 チャレンジ 1の方法と同様に、この方法もお勧めしません。 秘密をハードコーディングするほど悪くはありませんが、いくつかの弱点があることが分かります。

コンテナに環境変数を渡す方法は 4 つあります。

  • Dockerfile内のENVステートメントを使用して変数の置換を行います (ビルドされるすべてのイメージの変数を設定します)。 例えば:

    環境変数ポート$ポート
  • docker runコマンドで‑eフラグを使用します。 例えば:

    docker run -e パスワード=123 mycontainer
  • Docker Compose YAML ファイルで環境キーを使用します。
  • 変数を含む.envファイルを使用します。

このチャレンジでは、環境変数を使用してJWT を設定し、コンテナーを調べてJWT が公開されているかどうかを確認します。

環境変数を渡す

  1. API クライアント ディレクトリに戻ります。

    cd ~/microservices-march/auth/apiclient
  2. このチャレンジのアプリ (環境変数を使用するもの) を作業ディレクトリにコピーし、チャレンジ 1 のapp.pyファイルを上書きします。

    cp ./app_versions/medium_environment_variables.py ./app.py
  3. アプリを見てみましょう。出力の関連行では、シークレット (JWT) がローカル コンテナー内の環境変数として読み取られます。

    cat app.py ... jwt = "" の場合、os.environ に "JWT" がある場合: jwt = "Bearer " + os.environ.get("JWT") ...
  4. 上記で説明したように、環境変数をコンテナに取り込む方法はいくつかあります。 一貫性を保つために、Docker Compose を使用します。 環境キーを使用してJWT環境変数を設定する Docker Compose YAML ファイルの内容を表示します。

    cat docker-compose.env.yml --- バージョン: "3.9" サービス: apiclient: ビルド: . イメージ: apiclient extra_hosts: - "host.docker.internal:host-gateway" 環境: - JWT
  5. 環境変数を設定せずにアプリを実行します。 の401出力の最後から 2行目の不正なコードは、API クライアント アプリが JWT を渡さなかったために認証が失敗したことを示しています。

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 権限のないapiclient-apiclient-1 がコード 0 で終了しました
  6. 簡単にするために、環境変数をローカルに設定します。 現時点ではセキュリティ上の問題ではないため、チュートリアルのこの時点ではこれを実行しても問題ありません。

    エクスポート JWT=`cat token1.jwt`
  7. コンテナを再度実行します。 これでテストは成功し、チャレンジ 1 と同じメッセージが表示されます。

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 成功 apiKey1 apiclient-apiclient-1 はコード 0 で終了しました

したがって、少なくともベース イメージにはシークレットが含まれておらず、実行時にシークレットを渡すことができるため、より安全です。 しかし、まだ問題が残っています。

コンテナを調べる

  1. コンテナ イメージに関する情報を表示して、API クライアント アプリのコンテナ ID を取得します (読みやすくするために出力は 2 行に分かれています)。

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"コンテナ ID 名前 イメージ ...
    6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... 作成ステータス... 6 分前 終了しました (0) 6 分前 ... 約1時間前 約1時間前
  2. APIクライアントアプリのコンテナを検査します。 <コンテナID>の値を代入します 容器 ID 上記の出力のフィールド(ここでは 6b20c75830df)。

    docker inspectコマンドを使用すると、現在実行中かどうかに関係なく、起動されたすべてのコンテナを検査できます。 そして、それが問題です。コンテナが実行されていない場合でも、出力によってEnv配列内の JWT が公開され、コンテナ構成に安全でない方法で保存されます。

    docker examine <コンテナ ID> ...
    "Env": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "PATH=/usr/local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", 「PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...」、「PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c...」]

課題3: ローカルシークレットを使用する

ここまでで、シークレットをハードコーディングしたり環境変数を使用したりすることは、あなた (またはあなたのセキュリティ チーム) が望むほど安全ではないことがわかりました。

セキュリティを強化するには、ローカル Docker シークレットを使用して機密情報を保存してみてください。 繰り返しますが、これはゴールドスタンダードな方法ではありませんが、どのように機能するかを理解しておくとよいでしょう。 本番環境で Docker を使用しない場合でも、コンテナからシークレットを抽出することを困難にする方法が重要なポイントです。

Docker では、シークレットはファイル システム mount /run/secrets/を介してコンテナーに公開されます。このファイル システムには、各シークレットの値を含む個別のファイルがあります。

このチャレンジでは、Docker Compose を使用してローカルに保存されたシークレットをコンテナーに渡し、このメソッドが使用されているときにシークレットがコンテナー内で表示されないことを確認します

ローカルに保存されたシークレットをコンテナに渡す

  1. ここまでで予想できたと思いますが、まずはapiclientディレクトリに移動します。

    cd ~/microservices-march/auth/apiclient
  2. このチャレンジのアプリ (コンテナー内のシークレットを使用するアプリ) を作業ディレクトリにコピーし、チャレンジ 2 のapp.pyファイルを上書きします。

    cp ./app_versions/better_secrets.py ./app.py
  3. /run/secrets/jotファイルから JWT 値を読み取る Python コードを見てみましょう。 (そして、おそらくファイルに 1 行しかないことを確認する必要があります。 おそらく 2024 年 3 月のマイクロサービスでしょうか?

    cat app.py ... jotfile = "/run/secrets/jot" jwt = "" if os.path.isfile(jotfile): with open(jotfile) as jwtfile: for line in jwtfile: jwt = "Bearer " + line ...

    さて、この秘密をどうやって作るのでしょうか? 答えはdocker-compose.secrets.ymlファイルにあります。

  4. Docker Compose ファイルを見てみましょう。ここでは、シークレット ファイルがsecretsセクションで定義され、 apiclientサービスによって参照されています。

    cat docker-compose.secrets.yml --- バージョン: "3.9" シークレット: jot: ファイル: token1.jwt サービス: apiclient: ビルド: . extra_hosts: - "host.docker.internal:host-gateway" シークレット: - jot

コンテナ内でシークレットが表示されていないことを確認する

  1. アプリを実行します。コンテナ内で JWT にアクセスできるようにしたので、次のようなおなじみのメッセージが表示されて認証が成功します。

    docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 成功 apiKey1 apiclient-apiclient-1 はコード 0 で終了しました
  2. コンテナ イメージに関する情報を表示し、API クライアント アプリのコンテナ ID をメモします (サンプル出力については、チャレンジ 2 の「コンテナを調べる」の手順 1 を参照してください)。

    docker ps -a --format "テーブル {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  3. APIクライアントアプリのコンテナを検査します。 <コンテナID>の値を代入します 容器 ID 前のステップの出力のフィールド。 「コンテナの調査」のステップ 2 の出力とは異なり、 Envセクションの先頭にJWT=行がありません。

    docker examine <コンテナ ID> "Env": [ "PATH=/usr/local/bin:/usr/local/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", 「PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c...」]

    ここまでは順調ですが、秘密はコンテナのファイルシステム/run/secrets/jotにあります。 おそらく、チャレンジ 1 の「コンテナ イメージからシークレットを取得する」と同じ方法を使用して、そこからシークレットを抽出できるでしょう。

  4. 抽出ディレクトリ (チャレンジ 1 で作成したもの) に変更し、コンテナーをtarアーカイブにエクスポートします。

    cd extractdocker export -o api2.tar <コンテナID>
  5. tarファイル内の secrets ファイルを探します。

    tar tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mon DD hh :mm run/secrets/jot

    ああ、JWT を含むファイルが表示されています。 コンテナ内に秘密を埋め込むことは「安全」だと言いましたよね? 状況はチャレンジ 1 と同じくらい悪いですか?

  6. では、 tarファイルから secrets ファイルを抽出し、その内容を見てみましょう。

    tar --extract --file=api2.tar を実行する /secrets/jotcat を実行する /secrets/jot

    良いニュースです! catコマンドからの出力はありません。つまり、コンテナ ファイル システム内のrun/secrets/jotファイルは空であり、そこには秘密は存在しません。 コンテナ内に秘密のアーティファクトが存在する場合でも、Docker はコンテナ内に機密データを保存しないほどスマートです。

とはいえ、このコンテナ構成は安全ですが、欠点が 1 つあります。 これは、コンテナを実行するときに、ローカル ファイル システムにtoken1.jwtというファイルが存在するかどうかによって決まります。 ファイルの名前を変更すると、コンテナの再起動が失敗します。 ([削除ではなく] token1.jwt の名前を変更し、手順 1 のdocker composeコマンドを再度実行することで、これを自分で試すことができます。)

つまり、半分のところまで来ました。コンテナは、簡単に侵害されないように秘密情報を使用しますが、ホスト上では秘密情報はまだ保護されていません。 秘密を暗号化せずにプレーンテキスト ファイルに保存することは望ましくありません。 秘密管理ツールを導入する時期が来ました。

課題4: シークレットマネージャーを使用する

シークレット マネージャーは、シークレットのライフサイクル全体にわたってシークレットを管理、取得、ローテーションするのに役立ちます。 選択できるシークレット マネージャーは多数あり、それらはすべて同様の目的を果たします。

  • 秘密を安全に保管する
  • アクセスを制御する
  • 実行時に配布する
  • シークレットローテーションを有効にする

シークレット管理のオプションは次のとおりです。

簡単にするために、このチャレンジでは Docker Swarm を使用しますが、原則は多くのシークレット マネージャーで同じです。

このチャレンジでは、 Docker でシークレットを作成しシークレットと API クライアント コードをコピーしコンテナーをデプロイしシークレットを抽出できるかどうかを確認し、シークレットをローテーションします

Dockerシークレットを設定する

  1. これまでの慣例に従い、 apiclientディレクトリに変更します。

    cd ~/microservices-march/auth/apiclient
  2. Docker Swarm を初期化します。

    docker swarm init Swarm が初期化されました: 現在のノード (t0o4eix09qpxf4ma1rrs9omrm) がマネージャーになりました。...
  3. シークレットを作成し、 token1.jwtに保存します。

    docker secret で jot ./token1.jwt qe26h73nhb35bak5fr5east27 を作成します。
  4. シークレットに関する情報を表示します。 シークレット値 (JWT) 自体は表示されないことに注意してください。

    docker secret examine jot [ { "ID": "qe26h73nhb35bak5fr5east27", "バージョン": { "インデックス": 11 }, "作成日時": 「 YYYY - MM - DD T hh : mm : ss . ms Z」、 「UpdatedAt」: " YYYY - MM - DD T hh : mm : ss . ms Z", "Spec": { "Name": "jot", "Labels": {} } } ]

Dockerシークレットを使用する

API クライアント アプリケーション コードで Docker シークレットを使用することは、ローカルで作成されたシークレットを使用することとまったく同じです。/run/secrets/ ファイルシステムから読み取ることができます。 必要なのは、Docker Compose YAML ファイル内のシークレット修飾子を変更することだけです。

  1. Docker Compose YAML ファイルを見てみましょう。 外部フィールドの値がtrue であることに注目してください。これは、Docker Swarm シークレットを使用していることを示しています。

    cat docker-compose.secretmgr.yml --- バージョン: "3.9" シークレット: jot: external: true サービス: apiclient: build: . イメージ: apiclient extra_hosts: - "host.docker.internal:host-gateway" シークレット: - jot

    したがって、この Compose ファイルは既存の API クライアント アプリケーション コードで動作することが期待できます。 まあ、ほぼそうです。 Docker Swarm (またはその他のコンテナ オーケストレーション プラットフォーム) は多くの付加価値をもたらしますが、複雑さも増します。

    docker compose は外部シークレットでは機能しないため、いくつかの Docker Swarm コマンド、特にdocker stack deploy を使用する必要があります。 Docker Stack はコンソール出力を非表示にするため、出力をログに書き込んでからログを検査する必要があります。

    作業を簡単にするために、コンテナを実行し続けるために継続的なwhile Trueループも使用します。

  2. このチャレンジのアプリ (シークレット マネージャーを使用するアプリ) を作業ディレクトリにコピーし、チャレンジ 3 のapp.pyファイルを上書きします。 app.pyの内容を表示すると、コードはチャレンジ 3 のコードとほぼ同じであることがわかります。 唯一の違いは、 while Trueループが追加されていることです。

    cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py ... while True: time.sleep(5) try: with urllib.request.urlopen(req) as response: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message, file=sys.stderr) except urllib.error.URLError as e: print(str(e.code) + " " + e.msg, file=sys.stderr)

コンテナをデプロイしてログを確認する

  1. コンテナをビルドします (以前のチャレンジでは Docker Compose がこれを処理しました)。

    docker build -t apiclient を実行します。
  2. コンテナをデプロイします。

    docker stack deploy --compose-file docker-compose.secretmgr.yml secretstackネットワーク secretstack_default を作成しています サービス secretstack_apiclient を作成しています
  3. 実行中のコンテナを一覧表示し、 secretstack_apiclientのコンテナ ID をメモします (前と同様に、読みやすくするために出力は複数の行に分散されています)。

    docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"コンテナ ID ...  
    20d0c83a8b86 ... ad9bdc05b07c ... ... 名前 ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... exciting_clarke ... ... 画像作成ステータス... apiclient:latest 31 秒前 30 秒前アップ... apiserver 2 時間前 2 時間前アップ
  4. Dockerログファイルを表示します。 <コンテナID>の値を代入します 容器 ID 前のステップの出力のフィールド(ここでは、 20d0c83a8b86)。 アプリケーション コードにwhile Trueループを追加したため、ログ ファイルには一連の成功メッセージが表示されます。 コマンドを終了するには、 Ctrl+c を押します。

    docker logs -f <コンテナID> 200 成功 apiKey1 200 成功 apiKey1 200 成功 apiKey1 200 成功 apiKey1 200 成功 apiKey1 200 成功 apiKey1 ... ^c

秘密にアクセスしてみる

機密性の高い環境変数は設定されていないことがわかっています (ただし、チャレンジ 2 の「コンテナーを調べる」のステップ 2 のように、 docker inspectコマンドを使用していつでも確認できます)。

チャレンジ 3 から、 /run/secrets/jotファイルが空であることもわかっていますが、以下を確認できます。

cd extractdocker export -o api3.tar 
tar --extract --file=api3.tar run/secrets/jot
cat run/secrets/jot

成功! コンテナからシークレットを取得したり、Docker シークレットから直接読み取ったりすることはできません。

秘密を回転させる

もちろん、適切な権限があれば、サービスを作成し、シークレットをログに読み込んだり、環境変数として設定したりするように構成できます。 さらに、API クライアントとサーバー間の通信は暗号化されていない (プレーン テキスト) ことにお気づきかもしれません。

したがって、ほとんどすべての秘密管理システムでは、秘密の漏洩は依然として起こり得ます。 結果として生じる損害の可能性を制限する 1 つの方法は、シークレットを定期的にローテーション (交換) することです。

Docker Swarm では、シークレットを削除して再作成することしかできません (Kubernetes ではシークレットの動的な更新が可能です)。 実行中のサービスに添付されたシークレットを削除することもできません。

  1. 実行中のサービスを一覧表示します。

    docker サービス ls ID 名前 モード ... sl4mvv48vgjz secretstack_apiclient が複製されました ... ... レプリカ イメージ ポート... 1/1 apiclient:最新
  2. secretstack_apiclientサービスを削除します。

    docker サービス rm secretstack_apiclient
  3. シークレットを削除し、新しいトークンで再作成します。

    docker secret rm jot
    docker secret create jot ./token2.jwt
  4. サービスを再作成します。

    docker スタックデプロイ --compose-file docker-compose.secretmgr.yml secretstack
  5. apiclientのコンテナ ID を検索します (サンプル出力については、 「コンテナのデプロイとログの確認」の手順 3 を参照してください)。

    docker ps --format "テーブル {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  6. 一連の成功メッセージを示す Docker ログ ファイルを表示します。 のために <コンテナID>の値を代入します 容器 ID 前のステップの出力のフィールド。 コマンドを終了するには、 Ctrl+c を押します。

    docker logs -f <コンテナID> 200 Success apiKey2 200 Success apiKey2 200 Success apiKey2 200 Success apiKey2 ... ^c

apiKey1からapiKey2への変更がわかりますか? 秘密をローテーションしました。

このチュートリアルでは、API サーバーは引き続き両方の JWT を受け入れますが、実稼働環境では、JWT 内のクレームに特定の値を要求したり、JWT の有効期限を確認したりすることで、古い JWT を非推奨にすることができます。

また、シークレットを更新できるシークレット システムを使用している場合は、新しいシークレット値を取得するために、コードでシークレットを頻繁に再読み込みする必要があることにも注意してください。

掃除

このチュートリアルで作成したオブジェクトをクリーンアップするには:

  1. secretstack_apiclientサービスを削除します。

    docker サービス rm secretstack_apiclient
  2. シークレットを削除します。

    docker シークレット rm jot
  3. 群れをそのままにしておきます (このチュートリアルのためだけに群れを作成したと仮定します)。

    docker swarm を強制的に終了する
  4. 実行中のapiserverコンテナを強制終了します。

    docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
  5. 不要なコンテナをリストして削除します。

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <コンテナID>
  6. 不要なコンテナ イメージを一覧表示して削除します。

    docker イメージリスト docker イメージ rm <画像ID>

次のステップ

このブログを使用して、独自の環境にチュートリアルを実装することも、ブラウザベースのラボで試すこともできます (こちらから登録してください)。 Kubernetes サービスの公開に関するトピックについて詳しく学習するには、ユニット 2 の他のアクティビティに従ってください。 マイクロサービス シークレット管理 101

NGINX Plus を使用した本番環境レベルの JWT 認証の詳細については、ドキュメントを確認し、ブログの「JWT と NGINX Plus を使用した API クライアントの認証」をお読みください。


「このブログ投稿には、入手できなくなった製品やサポートされなくなった製品が参照されている場合があります。 利用可能な F5 NGINX 製品およびソリューションに関する最新情報については、 NGINX 製品ファミリーをご覧ください。 NGINX は現在 F5 の一部です。 以前の NGINX.com リンクはすべて、F5.com の同様の NGINX コンテンツにリダイレクトされます。"