この投稿は、「Microservices March 2023:マイクロサービスの提供を始める」のコンセプトを実践するための4つのチュートリアルのうちの1つです:
- マイクロサービスをデプロイして構成する方法
- コンテナ内のシークレットを安全に管理する方法(本記事)
- GitHub Actionsを使用してマイクロサービスのカナリアリリースを自動化する方法
- OpenTelemetryトレーシングを使用してマイクロサービスを把握する方法
多くのマイクロサービスでは、安全に動作するためにシークレットが必要です。シークレットの例としては、SSL/TLS証明書の秘密鍵、別のサービスへの認証に使用するAPIキー、リモートログイン用のSSHキーなどがあります。シークレットを適切に管理するには、シークレットを使用するコンテキストを必要な箇所のみに厳しく制限し、必要な場合を除いてアクセスできないようにする必要があります。しかし、アプリケーション開発を急ぐあまり、この対策が省略されることも少なくありません。その結果、シークレットの不適切な管理が、情報漏えいやエクスプロイトの一般的な原因となっています。
チュートリアルの概要
このチュートリアルでは、クライアントコンテナがサービスへのアクセスに使用するJSON Web Token(JWT)を安全に配布し、使用する方法を説明します。4つの課題を通じて、シークレットを管理する4つの異なる方法を試し、コンテナ内でシークレットを適切に管理する方法だけでなく、不適切な方法についても学びます。
このチュートリアルではシークレットの例としてJWTを使用しますが、ここで紹介する手法は、データベースの認証情報、SSL秘密鍵、その他のAPIキーなど、コンテナで秘密にしておく必要があるあらゆる情報に適用できます。
このチュートリアルでは、主に2つのソフトウェアコンポーネントを使用します。
- APIサーバー – NGINX Open Sourceと、JWTからクレームを抽出する基本的なNGINX JavaScriptコードを実行するコンテナ。いずれかのクレームの値を返し、クレームが存在しない場合はエラーメッセージを返します
- APIクライアント – APIサーバーに対して単純に
GETリクエストを行う、非常にシンプルなPythonコードを実行するコンテナ
この動画で、チュートリアルの実演をご覧ください。
このチュートリアルを最も簡単に実施するには、Microservices Marchに登録し、提供されているブラウザベースのラボを使用します。この記事では、ご自身の環境でチュートリアルを実行する手順を説明します。
前提条件とセットアップ
前提条件
ご自身の環境でこのチュートリアルを完了するには、以下が必要です。
- Linux/Unix互換環境
- Linuxコマンドラインに関する基本的な知識
- 次のようなテキストエディター:
nanoまたはvim - Docker(Docker ComposeおよびDocker Engine Swarmを含む)
curl(ほとんどのシステムにインストール済み)git(ほとんどのシステムにインストール済み)
注記:
- このチュートリアルでは、ポート80でリッスンするテストサーバーを使用します。ポート80をすでに使用している場合は、テストサーバーを
‑pフラグで別のポートに設定し、dockerrunコマンドで起動します。その後、curlコマンド内のlocalhostにサフィックス:<port_number>を追加します。 - このチュートリアルでは、コマンドをターミナルにコピー&ペーストしやすいよう、Linuxコマンドラインのプロンプトを省略しています。チルダ(
~)はホームディレクトリを表します。
セットアップ
このセクションでは、チュートリアルリポジトリをクローンし、認証サーバーを起動し、トークンを使用する場合と使用しない場合のテストリクエストを送信します。
チュートリアルリポジトリのクローン
- ホームディレクトリにmicroservices-marchディレクトリを作成し、その中にGitHubリポジトリをクローンします。(別のディレクトリ名を使用し、それに応じて手順を読み替えることもできます。)このリポジトリには、設定ファイルと、シークレットの取得方法が異なる複数のAPIクライアントアプリケーションが含まれています。
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git- シークレットを表示します。これは署名付き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: Bearerを付けます。
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"認証サーバーのビルドと起動
- 認証サーバーのディレクトリに移動します:
cd apiserver- 認証サーバー用のDockerイメージをビルドします(末尾のピリオドに注意してください):
docker build -t apiserver .- 認証サーバーを起動し、実行中であることを確認します(読みやすいように、出力は複数行に分けて表示しています):
docker run -d -p 80:80 apiserver docker ps
CONTAINER ID IMAGE COMMAND ...
2b001f77c5cb apiserver "nginx -g 'daemon of..." ...
... CREATED STATUS ...
... 26 seconds ago Up 26 seconds ...
... PORTS ...
... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ...
... NAMES
... relaxed_proskuriakova認証サーバーのテスト
- 認証サーバーがJWTを含まないリクエストを拒否し、
401AuthorizationRequiredを返すことを確認します:
curl -X GET http://localhost<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.23.3</center>
</body>
</html>- JWTは
Authorizationヘッダーを使用して指定します。200OKリターンコードは、APIクライアントアプリの認証が成功したことを示します。
curl -i -X GET -H "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhostHTTP/1.1 200 OK
Server: nginx/1.23.2
Date: Day, DD Mon YYYY hh:mm:ss TZ
Content-Type: text/html
Content-Length: 64
Last-Modified: Day, DD Mon YYYY hh:mm:ss TZ
Connection: keep-alive
ETag: "63dc0fcd-40"
X-MESSAGE: Success apiKey1
Accept-Ranges: bytes
{ "response": "success", "authorized": true, "value": "999" }課題1:アプリにシークレットをハードコードする(のはNG!)
この課題を始める前に、はっきりさせておきましょう。アプリにシークレットをハードコードするのは非常に危険です。コンテナイメージにアクセスできる人なら誰でも、ハードコードされた認証情報を簡単に見つけて抽出できることを確認します。
このチャレンジでは、APIクライアントアプリのコードをコピーしてビルドディレクトリに配置し、アプリをビルドして実行した後、シークレットを抽出します。
APIクライアントアプリをコピーする
The app_versionsは、apiclientディレクトリのサブディレクトリです。4つのチャレンジで使用するシンプルなAPIクライアントアプリの各バージョンが格納されており、バージョンが進むごとにセキュリティが少しずつ強化されています(詳細は「チュートリアルの概要」を参照してください)。
- APIクライアントのディレクトリに移動します:
cd ~/microservices-march/auth/apiclient- このチャレンジで使用するアプリ(シークレットがハードコードされているアプリ)を作業ディレクトリにコピーします:
cp ./app_versions/very_bad_hard_code.py ./app.py- アプリを確認します:
cat app.pyimport 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)
try:
with urllib.request.urlopen(req) as response:
the_page = response.read()
message = response.getheader("X-MESSAGE")
print("200 " + message)
except urllib.error.URLError as e:
print(str(e.code) + " s " + e.msg)- このコードはローカルホストにリクエストを送信し、成功メッセージまたはエラーコードを出力するだけです。このリクエストでは、次の行で「
Authorization」ヘッダーを追加します。
req.add_header("Authorization", authstring)- ほかに何か気づきましたか?ハードコードされたJWTがあるようです。これについては後ほど説明します。まずはアプリをビルドして実行しましょう。
APIクライアントアプリをビルドして実行
ここでは、docker composeコマンドとDocker Compose YAMLファイルを使用します。これにより、処理内容を少し把握しやすくなります。
(前のセクションのステップ2では、チャレンジ1専用のAPIクライアントアプリのPythonファイル(very_bad_hard_code.py)の名前をapp.pyに変更しました。他の3つのチャレンジでも同様に変更します。毎回app.pyを使用すると、Dockerfileを変更する必要がないため、作業が簡単になります。ただし、毎回コンテナを強制的に再ビルドするには、‑build引数をdocker composeコマンドに含める必要があります。)
The docker composeコマンドは、コンテナをビルドしてアプリケーションを起動し、APIリクエストを1回実行した後、API呼び出しの結果をコンソールに表示してコンテナを停止します。
出力の最後から2行目にある200 Successコードは、認証が成功したことを示します。さらに、apiKey1の値は、認証サーバーがJWT内の同名のクレームをデコードできたことを示しており、認証成功の裏付けとなります。
docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0このように、ハードコードされた認証情報はAPIクライアントアプリで正しく機能しました。これは当然の結果です。しかし、セキュリティ上の問題はないのでしょうか。コンテナは終了前にこのスクリプトを1回だけ実行し、シェルも備えていないため、問題ないのかもしれません。
実際には、まったくセキュアではありません。
コンテナイメージからシークレットを取得する
認証情報をハードコーディングすると、コンテナイメージにアクセスできる人なら誰でもその内容を確認できる状態になります。コンテナのファイルシステムは簡単に抽出できるためです。
- Create the extractディレクトリを作成し、そのディレクトリに移動します:
mkdir extractcd extract- コンテナイメージの基本情報を一覧表示します。
--formatフラグを使用すると出力が読みやすくなります(同じ理由から、ここでは出力を2行に分けています)。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID NAMES IMAGE ...
11b73106fdf8 apiclient-apiclient-1 apiclient ...
ad9bdc05b07c exciting_clarke apiserver ...
... CREATED STATUS
... 6 minutes ago Exited (0) 4 minutes ago
... 43 minutes ago Up 43 minutes- 最新のapiclientイメージを.tarファイルとして抽出します。
<container_ID>には、上記の出力にあるCONTAINERIDフィールドの値(このチュートリアルでは11b73106fdf8)を指定します:
docker export -o api.tar <container_ID>- コンテナのファイルシステム全体を含むapi.tarアーカイブの作成には数秒かかります。シークレットを見つける方法の1つは、アーカイブ全体を展開して解析することです。しかし、関心のある情報を見つけるには、もっと簡単な方法があります。それは、
dockerhistoryコマンドでコンテナの履歴を表示する方法です。(この方法は、Docker Hubやその他のコンテナレジストリで見つけたコンテナにも使用できるため、特に便利です。このようなコンテナでは、Dockerfileがなく、コンテナイメージしか入手できない場合があります)。 - コンテナの履歴を表示します:
docker history apiclient
IMAGE CREATED ...
9396dde2aad0 8 minutes ago ...
<missing> 8 minutes ago ...
<missing> 28 minutes ago ...
... CREATED BY SIZE ...
... CMD ["python" "./app.py"] 622B ...
... COPY ./app.py ./app.py # buildkit 0B ...
... WORKDIR /usr/app/src 0B ...
... COMMENT
... buildkit.dockerfile.v0
... buildkit.dockerfile.v0
... buildkit.dockerfile.v0- 出力行は新しいものから順に並んでいます。この出力から、作業ディレクトリが「/usr/app/src」に設定された後、アプリのPythonコードファイルがコピーされ、実行されたことがわかります。このコンテナの中核となるコードベースが「/usr/app/src/app.py」にあることは容易に推測できるため、認証情報もそこに存在する可能性があります。
- この情報を基に、そのファイルだけを抽出します:
tar --extract --file=api.tar usr/app/src/app.py- ファイルの内容を表示します。これだけで、「セキュア」なJWTにアクセスできました:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...課題2:シークレットを環境変数として渡す(これもNG!)
Microservices March 2023のUnit 1(マイクロサービスアーキテクチャへのTwelve-Factor Appの適用)を修了した方は、環境変数を使用してコンテナに構成データを渡す方法をご存じでしょう。見逃した方もご安心ください。登録後にオンデマンドでご覧いただけます。
この課題では、シークレットを環境変数として渡します。課題1の方法と同様、この方法も推奨しません。シークレットをハードコーディングするほど問題ではありませんが、後ほど説明するように、いくつかの弱点があります。
コンテナに環境変数を渡す方法は4つあります。
- 変数置換を行うには、
ENVステートメントをDockerfileで使用します(ビルドされるすべてのイメージに変数を設定します)。例:
ENV PORT $PORT- Use the
‑eフラグをdockerrunコマンドで使用します。例:
docker run -e PASSWORD=123 mycontainer- Docker ComposeのYAMLファイルで
environmentキーを使用します。 - 変数を含む「.env」ファイルを使用します。
このチャレンジでは、環境変数を使用してJWTを設定し、コンテナを調べてJWTが露出していないか確認します。
環境変数を渡す
- APIクライアントのディレクトリに戻ります:
cd ~/microservices-march/auth/apiclient- このチャレンジで使用するアプリ(環境変数を使用するアプリ)を作業ディレクトリにコピーし、チャレンジ1の「app.py」ファイルを上書きします:
cp ./app_versions/medium_environment_variables.py ./app.py- アプリを確認します。該当する出力行では、シークレット(JWT)がローカルコンテナ内の環境変数として読み込まれています:
cat app.py...
jwt = ""
if "JWT" in os.environ:
jwt = "Bearer " + os.environ.get("JWT")
...- 前述のとおり、環境変数をコンテナに渡す方法はいくつかあります。ここでは一貫性を保つため、Docker Composeを使用します。次のDocker Compose YAMLファイルの内容を表示します。このファイルでは、
environmentキーを使用してJWT環境変数を設定しています。
cat docker-compose.env.yml---
version: "3.9"
services:
apiclient:
build: .
image: apiclient
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- JWT- 環境変数を設定せずにアプリを実行します。出力の最後から2行目にある
401Unauthorizedコードは、APIクライアントアプリがJWTを渡さなかったため、認証に失敗したことを示しています。
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1 | 401 Unauthorized
apiclient-apiclient-1 exited with code 0- 簡単にするため、環境変数をローカルに設定します。現時点で問題としているセキュリティ上の懸念事項ではないため、このチュートリアルのこの段階ではローカルに設定しても問題ありません:
export JWT=`cat token1.jwt`- コンテナを再度実行します。今度はテストが成功し、Challenge 1と同じメッセージが表示されます:
docker compose -f docker-compose.env.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0これで少なくとも、ベースイメージにシークレットが含まれなくなり、実行時に渡せるため、より安全になりました。しかし、まだ問題が残っています。
コンテナを確認する
- コンテナイメージの情報を表示し、APIクライアントアプリのコンテナIDを取得します(読みやすいように、出力は2行に分けています)。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID NAMES IMAGE ...
6b20c75830df apiclient-apiclient-1 apiclient ...
ad9bdc05b07c exciting_clarke apiserver ...
... CREATED STATUS
... 6 minutes ago Exited (0) 6 minutes ago
... About an hour ago Up About an hour- APIクライアントアプリのコンテナを調査します。
<container_ID>には、上記の出力にあるCONTAINERIDフィールドの値(ここでは6b20c75830df)を指定します。dockerinspectコマンドを使用すると、現在実行中かどうかにかかわらず、起動済みのすべてのコンテナを調査できます。ここに問題があります。コンテナが実行されていないにもかかわらず、出力では、コンテナ設定に安全でない状態で保存されたJWTがEnv配列に公開されます。
docker inspect <container_ID>...
"Env": [
"JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...",
"PATH=/usr/local/bin:/usr/local/sbin:/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 secretsを使用して機密情報を保存する方法を試すことができます。繰り返しになりますが、これは最善の方法ではないものの、その仕組みを理解しておくことは有益です。本番環境でDockerを使用しない場合でも、重要なのは、コンテナからシークレットを抽出しにくくする方法を理解することです。
Dockerでは、シークレットはファイルシステムのマウント先である「/run/secrets/」を介してコンテナに公開されます。このディレクトリには、各シークレットの値を格納した個別のファイルがあります。
この課題では、ローカルに保存されたシークレットをコンテナに渡します。その際にDocker Composeを使用し、続いて、この方法を使用した場合にシークレットがコンテナ内で表示されないことを確認します。
ローカルに保存されたシークレットをコンテナに渡す
- ここまでの手順から予想できるように、まずapiclientディレクトリに移動します。
cd ~/microservices-march/auth/apiclient- このチャレンジで使用するアプリ(コンテナ内からシークレットを使用するアプリ)を作業ディレクトリにコピーし、チャレンジ2の「app.py」ファイルを上書きします:
cp ./app_versions/better_secrets.py ./app.py- JWT値を読み取るPythonコードを確認してください。読み取り元は「/run/secrets/jot」ファイルです。(確かに、このファイルが1行だけであることも確認した方がよいでしょう。Microservices March 2024で取り上げるかもしれません。)
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ファイルにあります。
- Docker Composeファイルを確認すると、シークレットファイルが
secretsセクションで定義され、apiclientサービスから参照されています。
cat docker-compose.secrets.yml---
version: "3.9"
secrets:
jot:
file: token1.jwt
services:
apiclient:
build: .
extra_hosts:
- "host.docker.internal:host-gateway"
secrets:
- jotシークレットがコンテナ内に表示されないことを確認
- アプリを実行します。コンテナ内からJWTにアクセスできるようにしたため、認証が成功し、見慣れた次のメッセージが表示されます。
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0- コンテナイメージに関する情報を表示し、APIクライアントアプリのコンテナIDを確認します(出力例については、チャレンジ2の「コンテナを確認」のステップ1を参照してください):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- APIクライアントアプリのコンテナを調べます。
<container_ID>には、前の手順の出力にあるCONTAINERIDフィールドの値を指定します。コンテナの確認の手順2の出力とは異なり、Envセクションの先頭にJWT=行はありません。
docker inspect <container_ID>
"Env": [
"PATH=/usr/local/bin:/usr/local/sbin:/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にあります。Challenge 1の「コンテナイメージからシークレットを取得」と同じ方法で、そこから抽出できるかもしれません。
- Challenge 1で作成したextractディレクトリに移動し、コンテナをtarアーカイブにエクスポートします:
cd extractdocker export -o api2.tar <container_ID>- シークレットファイルが含まれているか、tarファイル内を確認します:
tar tvf api2.tar | grep jot-rwxr-xr-x 0 0 0 0 Mon DD hh:mm run/secrets/jot- おっと、JWTが含まれるファイルが見えてしまっています。シークレットをコンテナに埋め込むのは「セキュア」だと言っていませんでしたか?Challenge 1と同じくらい深刻な状況なのでしょうか?
- 確認してみましょう。tarファイルからシークレットファイルを抽出し、その内容を確認します。
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot- 朗報です。
catコマンドから出力がないため、コンテナのファイルシステム内にあるrun/secrets/jotファイルは空です。つまり、そこにシークレットはありません。コンテナ内にシークレットのアーティファクトが存在していても、Dockerは機密データをコンテナに保存しないよう適切に処理します。
とはいえ、このコンテナ構成はセキュアですが、1つ欠点があります。コンテナの実行時に、ローカルファイルシステムにtoken1.jwtというファイルが存在することが前提です。このファイルの名前を変更すると、コンテナを再起動できません。(実際に試すには、token1.jwtを削除せずに名前だけ変更し、ステップ1のdocker composeコマンドをもう一度実行してください。)
これで目標の半分までは達成できました。コンテナでは、シークレットが容易に侵害されないよう保護する方法で使用されていますが、ホスト上のシークレットは依然として保護されていません。シークレットを暗号化せずにプレーンテキストファイルへ保存することは避ける必要があります。ここで、シークレット管理ツールを導入します。
課題4:シークレット管理ツールを使用する
シークレットマネージャーは、シークレットのライフサイクル全体にわたる管理、取得、ローテーションを支援します。さまざまなシークレットマネージャーから選択できますが、いずれも同様の目的を果たします。
- シークレットを安全に保存
- アクセスを制御
- 実行時に配布する
- シークレットのローテーションを有効化
シークレット管理には、次のオプションがあります。
- すべてのクラウドプロバイダーがシークレットサービスを提供しています(例:AWS Secrets Manager、Google Cloud PlatformのSecret Manager、Microsoft AzureのKey Vault)
- KubernetesにはSecretオブジェクトがあります
- Hashicorp Vaultは、広く利用されているクロスプラットフォーム対応のシークレット管理ツールです
- OpenShiftにはシークレット管理サービスがあります
- Docker Swarmにはシークレットサービスがあります
簡略化のため、この課題ではDocker Swarmを使用しますが、この原則は多くのシークレット管理ツールにも共通します。
このチャレンジでは、Dockerでシークレットを作成し、シークレットとAPIクライアントコードをコピーし、コンテナをデプロイした後、シークレットを抽出できるか試し、シークレットをローテーションします。
Docker Secretの設定
- ここまでと同様に、apiclientディレクトリに移動します。
cd ~/microservices-march/auth/apiclient- Docker Swarmを初期化します:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...- シークレットを作成し、token1.jwtに保存します:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27- シークレットに関する情報を表示します。シークレット値(JWT)自体は表示されないことに注意してください:
docker secret inspect jot[
{
"ID": "qe26h73nhb35bak5fr5east27",
"Version": {
"Index": 11
},
"CreatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
"UpdatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
"Spec": {
"Name": "jot",
"Labels": {}
}
}
]Docker Secretを使用する
APIクライアントのアプリケーションコードでDocker Secretを使用する方法は、ローカルで作成したSecretを使用する場合とまったく同じです。Secretは/run/secrets/ファイルシステムから読み取れます。必要なのは、Docker Compose YAMLファイル内のSecret修飾子を変更することだけです。
- Docker Compose YAMLファイルを確認してください。
externalフィールドの値が「true」になっており、Docker Swarmシークレットを使用していることがわかります。
cat docker-compose.secretmgr.yml---
version: "3.9"
secrets:
jot:
external: true
services:
apiclient:
build: .
image: apiclient
extra_hosts:
- "host.docker.internal:host-gateway"
secrets:
- jot- したがって、このComposeファイルは既存のAPIクライアントアプリケーションコードで動作すると考えられます。ただし、ほぼ、という条件付きです。Docker Swarm(またはその他のコンテナオーケストレーションプラットフォーム)は多くの付加価値をもたらす一方で、複雑さも増します。
dockercomposeは外部シークレットに対応していないため、Docker Swarmのコマンド、特にdockerstackdeployを使用する必要があります。Docker Stackではコンソール出力が表示されないため、出力をログに書き込み、そのログを確認する必要があります。作業を簡単にするため、継続的なwhileTrueループも使用して、コンテナを実行し続けます。 - このチャレンジで使用するアプリ(シークレットマネージャーを使用するアプリ)を作業ディレクトリにコピーし、チャレンジ3の「app.py」ファイルを上書きします。「app.py」の内容を表示すると、コードはチャレンジ3のものとほぼ同じであることがわかります。唯一の違いは、「
whileTrue」ループが追加されている点です。
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)コンテナをデプロイしてログを確認する
- コンテナをビルドします(これまでのチャレンジでは、Docker Composeがこの処理を行っていました)。
docker build -t apiclient .- コンテナをデプロイします:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient- 実行中のコンテナを一覧表示し、secretstack_apiclientのコンテナIDを確認します(前述と同様に、読みやすいよう出力を複数行に分けています)。
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID ...
20d0c83a8b86 ...
ad9bdc05b07c ...
... NAMES ...
... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ...
... exciting_clarke ...
... IMAGE CREATED STATUS
... apiclient:latest 31 seconds ago Up 30 seconds
... apiserver 2 hours ago Up 2 hours- Dockerログファイルを表示します。
<container_ID>には、前の手順の出力にあるCONTAINERIDフィールドの値(ここでは20d0c83a8b86)を指定します。アプリケーションコードにwhileTrueループを追加したため、ログファイルには一連の成功メッセージが表示されます。コマンドを終了するには、Ctrl+cを押します。
docker logs -f <container_ID>200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
...
^cシークレットへのアクセスを試す
機密情報を含む環境変数が設定されていないことは確認済みです(ただし、Challenge 2の「docker inspect」のStep 2と同様に、Examine the Containerコマンドでいつでも確認できます)。
Challenge 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ではシークレットを動的に更新できます)。また、実行中のサービスに関連付けられているシークレットは削除できません。
- 実行中のサービスを一覧表示します:
docker service lsID NAME MODE ...
sl4mvv48vgjz secretstack_apiclient replicated ...
... REPLICAS IMAGE PORTS
... 1/1 apiclient:latest- Delete the secretstack_apiclientサービスを削除します。
docker service rm secretstack_apiclient- シークレットを削除し、新しいトークンで再作成します:
docker secret rm jot docker secret create jot ./token2.jwt- サービスを再作成します:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack- 次のコンテナIDを確認します:
apiclient(出力例については、「コンテナをデプロイしてログを確認する」の手順3を参照してください):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- 一連の成功メッセージが記録されたDockerログファイルを表示します。
<container_ID>には、前の手順の出力にあるCONTAINERIDフィールドの値を指定します。コマンドを終了するには、Ctrl+cを押します。
docker logs -f <container_ID>200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
...
^cSee the change from apiKey1からapiKey2への変更を確認できましたか?これでシークレットのローテーションは完了です。
このチュートリアルでは、APIサーバーは引き続き両方のJWTを受け入れますが、本番環境では、JWT内のクレームに特定の値を必須とするか、JWTの有効期限を確認することで、古いJWTを非推奨にできます。
また、シークレットを更新できるシークレット管理システムを使用している場合、新しいシークレット値を取得できるよう、コードでシークレットを頻繁に再読み込みする必要があります。
クリーンアップ
このチュートリアルで作成したオブジェクトをクリーンアップするには、次の手順を実行します。
- Delete the secretstack_apiclientサービスを削除します。
docker service rm secretstack_apiclient- シークレットを削除します。
docker secret rm jot- swarmから離脱します(このチュートリアル専用にswarmを作成したことを前提とします)。
docker swarm leave --force- 実行中のapiserverコンテナを停止します。
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill- 不要なコンテナを一覧表示してから削除します。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>- 不要なコンテナイメージを一覧表示し、削除します。
docker image list docker image rm <image_ID>次のステップ
このブログを参考に、ご自身の環境でチュートリアルを実施するか、F5のブラウザベースのラボでお試しいただけます(こちらから登録)。Kubernetesサービスの公開について詳しく学ぶには、ユニット2:マイクロサービスのシークレット管理入門のその他の演習もご覧ください。
NGINX Plusを使用した本番環境向けのJWT認証について詳しくは、ドキュメントをご覧ください。また、ブログ記事「NGINX PlusによるJWTを使用したAPIクライアントの認証」もご参照ください。

著者について

関連するブログ記事
チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。
AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。
JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。
F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。
NGINX PlusによるAWS Auto Scalingグループの負荷分散
新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります