BLOG | NGINX

NGINX チュートリアル: コンテナ環境での安全なシークレット管理方法

NGINX-Part-of-F5-horiz-black-type-RGB
Robert Haynes サムネール
Robert Haynes
Published July 11, 2023

この記事は、Microservices July 2023: マイクロサービスのStart Delivering Microservicesの方法を実行するための4つのチュートリアルの1つです。

 

 

お使いのマイクロサービスの多くには、安全な運用のためにシークレットが必要です。シークレットの例として、SSL/TLS証明書の秘密鍵、別のサービスで認証するAPIキー、またはリモートログイン用のSSHキーなどがあります。シークレットを適切に管理するには、シークレットを使用するコンテキストを必要な場所に制限する、およびシークレットを必要なとき以外にアクセスされるのを防止することが必要です。ただこのやり方は、アプリケーション開発が急がれる場合は、飛ばされることがよくあります。その結果はどうなるのでしょうか? シークレット管理が不適切なことで、情報漏洩や悪用される一般的な原因となります。

チュートリアルの概要

このチュートリアルでは、クライアントコンテナがサービスのアクセスに利用する JSON  Web Token(JWT)を、安全に配布および利用しています。このチュートリアルには課題が4つあり、シークレット管理において4つの方法それぞれで試行し、お使いのコンテナでの正しいシークレットの管理方法だけではなく、十分とはいえない方法についても確認します。

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

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

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

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

このチュートリアルを実行するもっとも簡単な方法は、Microservices Julyに登録し、提供されたブラウザベースのラボを使用することです。この記事には、お使いの環境でチュートリアルを実行する代替方法も記載されています。

前提条件とセットアップ

前提条件

お使いの環境でチュートリアルを完了するには、以下が必要です。

  • Linux/Unix 環境
  • Linux コマンドラインの基本を理解していること
  • nanoまたはvim等のテキストエディタ
  • Docker (Docker ComposeDocker Engine Swarmを含む)
  • curl (ほとんどのシステムではすでにインストール済み)
  • git (ほとんどのシステムではすでにインストール済み)

注:

  • チュートリアルは、ポート 80 につながるテストサーバーを利用します。ポート 80 をすでに使用している場合は、‑pフラグを使って、docker runコマンドで開始するときにテストサーバーに別の値を設定します。次に、curlコマンドで:<port_number>サフィックスをlocalhostに含めます。
  • チュートリアル全体で、Linux コマンドラインのプロンプトは省略され、コマンドをチュートリアルにコピー&ペーストしやすくしています。チルダ (~) はホーム ディレクトリを表します。

セットアップ

このセクションではチュートリアル リポジトリをcloneし、認証サーバーを起動、そしてトークン付きとトークンなしで、テストリクエストを送信します。

チュートリアルリポジトリをcloneする

  1. お使いのホームディレクトリで、microservices-marchディレクトリを作成し、GitHubリポジトリをそこにcloneします。(別のディレクトリ名を使用し、それにあわせて各操作を適宜変更することも可能です。) リポジトリには、構成ファイルと、別の方法でシークレットを取得する API クライアントアプリケーションの個別バージョンが含まれています。

    mkdir ~/microservices-marchcd ~/microservices-marchgit clone 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認証フレームワークを使って認証サーバーに渡します。そこではこの例のように、JWTにAuthorization: Bearerプレフィクスを付けます。

"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

認証サーバーを構築・起動する

  1. 認証サーバーディレクトリに移動します。

    cd apiserver
    
  2. 認証サーバー用Dockerイメージを作成します。(最後の「ピリオド(.)」に注意してください。)

    docker build -t apiserver .
    
  3. 認証サーバーを起動し、正しく実行されているかを確認します。(出力は読みやすいように複数ラインで表示されます。)

    docker run -d -p 80:80 apiserverdocker psCONTAINER 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
    

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

  1. 認証サーバーが JWT を含まないリクエストを拒否し、401 Authorization Requiredを返しているか確認します。

    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>
    
  2. Authorizationヘッダを使ってJWTを提供します。200 OKの応答コードはAPIクライアントアプリケーションが正常に認証されたことを示します。

    curl -i -X GET -H "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhostHTTP/1.1 200 OKServer: nginx/1.23.2Date: Day, DD Mon YYYY hh:mm:ss TZContent-Type: text/htmlContent-Length: 64Last-Modified: Day, DD Mon YYYY hh:mm:ss TZConnection: keep-aliveETag: "63dc0fcd-40"X-MESSAGE: Success apiKey1Accept-Ranges: bytes{ "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.pyimport urllib.requestimport urllib.errorjwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"authstring = "Bearer " + jwtreq = 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コマンドに含め、コンテナのリビルドを強制することを意味しています。)

docker composeコマンドは、コンテナを構築し、アプリケーションを起動し、API リクエストを1回行い、APIコールの結果をコンソールに表示しながら、コンテナをシャットダウンします。

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

docker compose -f docker-compose.hardcode.yml up -build...apiclient-apiclient-1  | 200  Success apiKey1apiclient-apiclient-1 exited with code 0

このように、ハードコード化された認証情報が API ライアントアプリケーションで正常に作動しましたが、これは驚くことではありません。しかし、これは安全でしょうか? コンテナが終了するまでに一度だけこのスクリプトが実行され、シェルを持っていないのだから、安全であると言えるかもしれないでしょうか?

実際のところ、これは安全とは言えません。

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

コンテナのファイルシステムを抜き出すのは簡単なことであり、認証情報をハードコードすると、コンテナイメージにアクセスできる人なら誰でも閲覧できる状態になります。

  1. extractディレクトリを作成して、移動する。

    mkdir extractcd extract
    
  2. コンテナイメージについての基本情報を表示します。--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
    
  3. 最新のapiclientイメージを.tarファイルとして抽出します。<container_ID> には、上記の出力にあるCONTAINER IDフィールド(このチュートリアルでは11b73106fdf8) の値を代入します。

    docker export -o api.tar <container_ID>
    

    コンテナのファイルシステム全体が含まれるapi.tarアーカイブの作成には数秒かかります。シークレットを探す方法の1つとして、アーカイブ全体を抽出して解析することができますが、面白そうなものをみつけるためのショートカットがあることが分かっています。docker historyコマンドでコンテナの履歴を表示することです。 (このショートカットは、Docker Hub または別のコンテナレジストリで、Dockerfileを持たずコンテナイメージのみを持つコンテナにも使えるため、特に便利です)。

  4. コンテナ履歴を表示する。

    docker history apiclientIMAGE         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にあり、それが認証情報の場所である可能性が高いことを推察するのは、優れた探偵でなくてもできることです。

  5. その知識により、そのファイルのみを抽出します。

    tar --extract --file=api.tar usr/app/src/app.py
    
  6. このようにファイルの内容を表示し、まさに“安全である” JWTへのアクセスを実施しました。

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

課題2: シークレットを環境変数として渡す (こちらも不適切です)

Microservices July 2023のUnit1 ”Twelve-Factor App” (アプリケーション構成ガイドライン)によるマイクロサービス・アーキテクチャの適用を完了した方は、環境変数を使って設定データをコンテナに渡すことを理解していると思います。もし見逃したとしてもご心配なく登録後にオンデマンドで利用できます。

この課題では、シークレットを環境変数として渡します。課題1で確認した方法のように、この方法も推奨されていません! シークレットのハードコード化ほど悪いものではありませんが、ご覧のとおり、弱点がいくつかあります。

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

  • DockerfileENVの記述を使い、変数の置換を行う (すべての構築イメージに変数を設定)。例:

    ENV PORT $PORT
    
  • ‑eフラグをdocker runコマンド上で使用する。例:

    docker run -e PASSWORD=123 mycontainer
    
  • environmentキーを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 = ""if "JWT" in os.environ:    jwt = "Bearer " + os.environ.get("JWT")...
    
  4. 上記のとおり、環境変数をコンテナに入れる方法が選択できます。一貫性を保つため、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
    
  5. 環境変数を設定しないでアプリケーションを実行します。APIクライアントアプリケーションがJWTを渡さなかったために認証に失敗したことが、出力の2行目から最後の401 Unauthorizedコードから確認できます。

    docker compose -f docker-compose.env.yml up -build...apiclient-apiclient-1  | 401  Unauthorizedapiclient-apiclient-1 exited with code 0
    
  6. 分かりやすくするため、環境変数をローカルで設定します。セキュリティ上の問題が今すぐ懸念されることがないため、チュートリアルのこの時点ではそうしてもよいでしょう。

    export JWT=`cat token1.jwt`
    
  7. コンテナを再び実行します。今回はテストに成功し、課題1と同じメッセージが表示されました。

    docker compose -f docker-compose.env.yml up -build...apiclient-apiclient-1  | 200  Success apiKey1apiclient-apiclient-1 exited with code 0
    

このため、少なくとも現時点ではベースイメージにはシークレットが含まれず、実行時に渡すことができるので、この方が安全です。ただし、まだ問題があります。

コンテナを検証する

  1. 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
    
  2. APIクライアントアプリケーションのコンテナを検査します。<container_ID>には、上の出力のCONTAINER IDフィールドから代入します(ここでは6b20c75830df)。

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

    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 シークレットを使用して機密情報を保存することができます。これも究極の方法ではありませんが、それがどのように機能するかを理解することは重要です。たとえ本番でDockerを使用しなくても、コンテナからシークレットを抽出するのをいかに難しくするか、ということが重要です。

Dockerでは、シークレットは/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コードを見てみます。

    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 Composファイルをご覧ください。ここでは、シークレットファイルはsecretsセクションで定義され、apiclientサービスが参照しています。

    cat docker-compose.secrets.yml---version: "3.9"secrets:  jot:    file: token1.jwtservices:  apiclient:    build: .    extra_hosts:      - "host.docker.internal:host-gateway"    secrets:      - jot
    

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

  1. アプリケーションを実行します。コンテナ内で JWT にアクセスできるようにしたため、認証はおなじみのメッセージで成功します。

    docker compose -f docker-compose.secrets.yml up -build...apiclient-apiclient-1  | 200 Success apiKey1apiclient-apiclient-1 exited with code 0
    
  2. コンテナイメージに関する情報を表示し、APIクライアントアプリケーションのコンテナIDをメモしてください。(出力例については、課題2の「コンテナを調べる」の手順1を参照してください)。

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
    
  3. APIクライアントアプリケーションのコンテナを調べます。<container_ID>には、前の手順の出力にあるCONTAINER IDフィールドの値を代入してください。「コンテナを調べる」の手順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にあります。課題1の「コンテナイメージからシークレットを取得する」と同じ方法を使用すれば、そこから抽出できるかもしれません。

  4. (課題1 で作成した)extractディレクトリに移動し、コンテナをtarアーカイブにエクスポートします。

    cd extractdocker export -o api2.tar <container_ID>
    
  5. tarファイルの中にあるシークレットを探します。

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

    なんと、JWTが含まれているファイルが表示されてしまいます。コンテナにシークレットを埋め込むことは「安全」だと言ったのに、課題1と同じような状況なのでしょうか?

  6. 見てみましょう – 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: シークレットマネージャーを使用する

シークレットマネージャーは、ライフサイクル全体を通じてシークレットの管理、検索、ローテーションを支援します。シークレットマネージャーには多くの種類があり、どれも似たような目的を果たすことができます。

  • シークレットの安全な保管
  • アクセスの制御
  • ランタイムへの配布
  • シークレットのローテーションを有効化

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

わかりやすくするために、この課題では Docker Swarmを使用しますが、原理は多くのシークレットマネージャーで共通です。

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

Dockerシークレットの設定

  1. これまでと同様、まずはapiclientディレクトリに移動します。

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

    docker swarm initSwarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager....
    
  3. シークレットを作成し、token1.jwtに格納します。

    docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27
    
  4. シークレットに関する情報を表示します。シークレット値 (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シークレットを使用する

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

  1. Docker Compose YAMLファイルを見てみましょう。externalフィールドにtrueという値があり、Docker Swarmのシークレットを使用していることがわかります。

    cat docker-compose.secretmgr.yml---version: "3.9"secrets:  jot:    external: trueservices:  apiclient:    build: .    image: apiclient    extra_hosts:      - "host.docker.internal:host-gateway"    secrets:      - 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 secretstackCreating network secretstack_defaultCreating service secretstack_apiclient
    
  3. 実行中のコンテナを一覧表示し、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
    
  4. Dockerログファイルを表示します。<container_ID>には、前の手順の出力にあるCONTAINER IDフィールドの値(ここでは、20d0c83a8b86)を代入してください。アプリケーションコードにwhile Trueループを追加したため、ログファイルには一連の成功メッセージが表示されています。Ctrl+cキーを押して、コマンドを終了します。

    docker logs -f <container_ID>200 Success apiKey1200 Success apiKey1200 Success apiKey1200 Success apiKey1200 Success apiKey1200 Success apiKey1...^c
    

シークレットへのアクセスを試みる

この課題では、機密性の高い環境変数を設定していません(ただし、課題 2 の「コンテナを調べる」の手順2のように、docker inspectコマンドでいつでも確認できます)。

課題3から、/run/secrets/jotファイルが空であることもわかっており、以下を確認できます。

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

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

シークレットのローテーション

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

そのため、ほとんどすべてのシークレット管理システムでシークレットの漏洩が発生する可能性があります。結果的に被害を受ける可能性を抑える方法の1つは、シークレットを定期的にローテーション(置換)することです。

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

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

    docker service lsID             NAME                    MODE         ...sl4mvv48vgjz   secretstack_apiclient   replicated   ...    ... REPLICAS   IMAGE              PORTS    ... 1/1        apiclient:latest
    
  2. secretstack_apiclientサービスを削除します。

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

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

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

    docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
    
  6. 一連の成功メッセージを示すDockerログファイルを表示します。<container_ID>には、前の手順の出力にあるCONTAINER IDフィールドの値を代入してください。Ctrl+cを押して、コマンドを終了します。

    docker logs -f <container_ID>200 Success apiKey2200 Success apiKey2200 Success apiKey2200 Success apiKey2...^c
    

apiKey1からapiKey2への変化を見てください。シークレットをローテーションしました。

このチュートリアルでは、APIサーバーはまだ両方のJWTを受け入れていますが、本番環境では、JWTのクレームに特定の値を要求したり、JWTの有効期限をチェックすることで古いJWTを非推奨にすることができます。

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

クリーンアップ

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

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

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

    docker secret rm jot
    
  3. swarmから抜けます(このチュートリアルのためだけにswarmを作成したと仮定)。

    docker swarm leave --force
    
  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 <container_ID>
    
  6. 不要なコンテナイメージを一覧表示して削除します。

    docker image listdocker image rm <image_ID>
    

次のステップ

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

NGINX Plusを使用したプロダクショングレードのJWT認証の詳細については、ドキュメントを確認ください。ブログの「Authenticating API Clients with JWT and NGINX Plus」もご参照いただけます。


"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."