BEMAロゴ

エンジニアの
成長を支援する
技術メディア

【Dockerセキュリティ入門】.envファイルの危険性と安全な代替手段:Secrets・Volumeで守る機密情報

こんにちは、株式会社メンバーズ DevOps Lead カンパニーの蔡です。

モーデンの開発プロセスにおいて、Dockerは既に不可欠な存在となっています。開発環境の構築やテスト環境や本番環境へのデプロイメントにしても、Dockerの使用は迅速で便利です。しかしながら、Dockerを応用するためには、考慮すべき点がいくつか存在します。

例えば、Dockerを利用した開発で、セキュリティについてどの程度意識しているでしょうか。特に、環境変数を記述する.envファイルの管理は、基本的ながらも多くのセキュリティ問題を引き起こす原因となっています。

Dockerを使ってアプリケーションを構築する中で、.envファイルを使って環境変数を管理するケースは多いと思います。
手軽に設定を切り替えられる便利な仕組みですが、一方で「本番環境でそのまま使って大丈夫?」と不安を感じたこともありました。
実際、環境変数の扱い方を誤ると、思わぬ情報漏れやセキュリティリスクにつながる可能性があります。

そこで今回は、.envファイルの仕組みを改めて整理しながら、その危険性とより安全な代替手段について紹介していきます。

envファイルで秘密情報を管理するリスク

.envファイルは、現代のWebアプリケーション開発において不可欠な要素です。その主な目的は、データベースの接続情報、APIキー、トークンといった、環境ごとに異なる設定値をアプリケーションのソースコード本体から分離して管理することにあります。開発者にとって、この仕組みは複数の環境設定を容易に切り替えることを可能になります。さらに.envファイルを共有することで、新しい開発者が安全にプロジェクトへ参加できるという利点もあります 。

.envファイルは利便性が高い一方で、重大なセキュリティリスクが潜んでいます。そのリスクをさらに深刻化させているのは、機密情報が暗号化されることなく、平文のまま保存されているということです。

このファイルは攻撃者にとって認証情報の宝庫であり、攻撃の主要な標的となります。ファイルへのアクセス権限を一度でも奪われると、攻撃者は何ら追加の解読作業を必要とせず、即座に認証情報を悪用できることを意味します。

Dockerでは、.envファイルがアプリケーションの設定情報をコンテナに渡すための重要な役割を担います。しかし、.envファイルは利便性が高い一方で、不適切な管理は深刻なセキュリティリスクを引き起こします。今回は単一ホスト(VM)上にDockerアプリケーションを構築する際に、.envファイル以外の機密情報の管理方法を考えます。

なぜ env ファイルで秘密情報を管理すべきではないのか

Docker環境でパスワードやAPIキーなどの秘密情報を.envファイル(環境変数)を利用して管理する方法は一般的ですが、セキュリティ上多くのリスクがあります。問題点を以下に挙げます:

  • 平文での保存:

    .env ファイルは通常、暗号化されずにプレーンテキストで保存されるため、ファイルにアクセスできれば誰でも簡単に中身を閲覧できてしまいます。

  • バージョン管理への誤コミット:

    .(ドット)から始まるファイル名は意図せずバージョン管理システム(Gitなど)にコミットされてしまうリスクがあり、過去のコミット履歴から秘密情報が漏洩する可能性があります。

  • 本番環境での漏洩:

    本番サーバーに直接ファイルを配置するため、サーバーへの不正アクセスがあった場合に、データベースのパスワードやAPIキーなどが一度に漏洩する危険性があります。

  • 複数ホスト間での秘密情報の管理:

    本番環境のサーバが複数台あり各ホストで.envファイルを使っている場合、パスワードのローテーション等で秘密情報を更新する際に、全てのホストで同期して書き換える必要があります。人手で各マシンの.envを編集・再展開するのは手間がかかりミスの元ですし、秘密情報が分散するほど運用負荷とリスクが高まります。集中管理の仕組みが必要です。

.envファイル以外の機密情報管理方法:Docker Swarm・Kubernetes

機密情報の問題に対応するために、一般的にDocker SwarmやKubernetesを使用して対処します。Docker SwarmやKubernetesのシークレット管理機能は、必要なコンテナにのみオンデマンドでメモリ上にマウントされ、コンテナ停止時には自動的に破棄されるため、機密情報が見られて漏洩する可能性が低くなります。クラスタ全体での一元管理により、手動配置作業が不要となり、サービス単位でのアクセス制御も可能です。バージョン管理や定期的なローテーション機能により、セキュリティポリシーの適用が容易になります。従来の.envファイル使用時のセキュリティリスクと管理の複雑さを大幅に軽減できます。

しかし、Docker SwarmやKubernetesの学習コストがかなり高く、クラスター管理、サービスディスカバリ、ネットワーキング、スケーリングなど、多くの高度な概念を理解する必要があります。また、追加のコンポーネントを動作させるために大量のCPUやメモリを消費します。単一ホストでのシンプルなデプロイの場合、Docker SwarmやKubernetesのような高機能なツールは過剰となり、かえって不向きな場合があります。こんな時、『Docker Compose Secrets 』という強力な選択肢になります。

Docker Compose Secretsについて

Docker Compose は、マルチコンテナのDocker アプリケーションを定義し、実行するツールです。コンテナを立ち上げる際に、パスワード、API キー、証明書などの機密情報の管理方法が大きな課題となります。

Docker Compose Secretsは、開発環境での機密情報管理を簡素化する機能です。従来の環境変数や.envファイルと異なり、機密情報をファイルとしてコンテナ内の/run/secrets/ディレクトリに一時的なファイルとしてマウントされます。これにより、以下のようなメリットが生まれます。

  • アクセスの制限: シークレットファイルへのアクセス権を必要とするプロセスだけに限定できます。
  • 秘密情報の非表示:Secretsの内容はdocker inspectの出力や環境変数リストには現れず、露出リスクを軽減します。
  • 一元管理: docker-compose.yml ファイル内で、どのサービスがどのシークレットにアクセスできるかを明示的に定義でき、管理が容易になります。

ただし注意すべき点は、Docker Compose SecretsがDocker Swarm SecretsやKubernetes Secretsのように全体的に暗号化される機能を持っていないことです。

Docker Compose Secretsの基本的な使用方法は以下の通りです:

version: '3.8'
services:
  app:
    image: myapp
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

トップレベルのsecretsキーでシークレットを定義し、ファイルを指定します。そして各サービスのsecretsキーで、そのサービスが利用するシークレットを指定します。

テストプロジェクトの選択

コンテナから機密情報を取得する方法をテストするため、適切なオープンソースプロジェクトを選択します。今回はn8nプロジェクトを選びました。

n8nは現在とても人気の高いワークフロー自動化プラットフォームのプロジェクトです。400以上の統合機能と、ネイティブAI機能を提供しています。14万1千スターを獲得し、最も人気のあるプロジェクトトップ50にランクインしています。オープンソースで透明性を保ちながら、商用利用も可能です。

n8nは様々なDockerイメージとの連携が可能で、機密情報の設定も必要とするため、今回のテスト目的に最適だと思います。

「docker compose secrets」で秘密情報を格納してみよう

secretsの仕組みをテストするために、今回はn8nとPostgresデータベースを連携するリポジトリを選びました。このリポジトリの.envファイルをdocker compose secretsで管理する形に変更します。

実行環境は Azure Virtual Machine で、OS は Ubuntu 24.04 LTS です。

今回用意する秘密情報は5個があります。「 postgres_db」、「postgres_non_root_password」、「postgres_non_root_user」、「postgres_password」、「 postgres_user」。
secretsをdocker-compose.ymlで運用する仕組みに変更したら、下記の仕組みになりました。

プロジェクトの構造は以下です。

n8n
├── docker-compose.yml
├── init-data.sh
└── secrets
    ├── postgres_db.txt
    ├── postgres_non_root_password.txt
    ├── postgres_non_root_user.txt
    ├── postgres_password.txt
    └── postgres_user.txt
└── .env

上記のファイルを配置したら、「docker compose up -d」コマンドで、n8nとPostgres Databaseのコンテナを起動できます。                      

docker-compose.yml

# docker-compose.ymlのバージョンは3.8以上でdocker compose secretsを利用できます。
version: '3.9'

services:
  # --------------------
  # 1. PostgreSQLデータベースのサービス定義
  # --------------------
  postgres:
    image: postgres:16
    restart: always    # コンテナが停止した場合、常に再起動する設定

    # --- secrets を使った環境変数の設定 ---
    # ここがsecrets活用の重要なポイントです。
    # 通常の環境変数(例: POSTGRES_USER=admin)の代わりに、末尾に `_FILE` をつけた環境変数を指定します。
    # これにより、PostgreSQLイメージは指定されたファイルパス(/run/secrets/...)から値を読み込み、それを設定値として使用します。
    environment:
      POSTGRES_USER_FILE: /run/secrets/postgres_user
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
      POSTGRES_DB_FILE: /run/secrets/postgres_db
      POSTGRES_NON_ROOT_USER_FILE: /run/secrets/postgres_non_root_user
      POSTGRES_NON_ROOT_PASSWORD_FILE: /run/secrets/postgres_non_root_password

    volumes:
      - db_storage:/var/lib/postgresql/data # DBデータを保存するためのボリューム
      - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh # DB初期化用のスクリプト

    healthcheck:
      # --- コンテナが正常に動作しているかを確認する設定です。 ---
      # ここでもsecretsファイルにアクセスして、DBユーザー名とDB名を取得しています。
      # 注意:`$$`について
      # docker-compose.ymlでは`$`は変数展開のために使われます。
      # シェルコマンド内で`$`をそのまま使いたい場合(ここでは`cat`コマンドの出力を`pg_isready`コマンドに渡すため)、
      test: ['CMD-SHELL', "pg_isready -h localhost -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)"]
      interval: 5s
      timeout: 5s
      retries: 10

    # --- このサービスに割り当てるsecretsの定義 ---
    # トップレベル(ファイルの一番下)で定義したsecretsの中から、
    # この `postgres` サービスが使用するものを指定します。
    # ここで指定したものが、コンテナ内の `/run/secrets/` 以下にファイルとしてマウントされます。
    # 例えば `postgres_user` は `/run/secrets/postgres_user` というファイルになります。
    secrets:
      - postgres_user
      - postgres_password
      - postgres_db
      - postgres_non_root_user
      - postgres_non_root_password

  # --------------------
  # 2. n8n(ワークフロー自動化ツール)のサービス定義
  # --------------------
  n8n:
    image: docker.n8n.io/n8nio/n8n # 使用するDockerイメージ
    restart: always

    environment:
      # .envファイルなどから読み込む通常の環境変数
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${TZ}
      DB_TYPE: ${DB_TYPE}
      DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST}
      DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT}
      # n8nのイメージもPostgreSQLと同様に、`_FILE` 接尾辞を使うことで、
      # ファイルからデータベースの接続情報を読み込むことができます。
      DB_POSTGRESDB_DATABASE_FILE: /run/secrets/postgres_db
      DB_POSTGRESDB_USER_FILE: /run/secrets/postgres_non_root_user
      DB_POSTGRESDB_PASSWORD_FILE: /run/secrets/postgres_non_root_password

    ports:
      - "5678:5678" # ホストの5678番ポートをコンテナの5678番ポートに接続

    volumes:
      - n8n_storage:/home/node/.n8n # n8nデータを保存するためのボリューム

    depends_on:
      # postgresサービスが正常起動(healthcheckが通る)してから、n8nサービスを起動させます。
      postgres:
        condition: service_healthy

    # --- n8nサービスに割り当てるsecrets ---
    # n8nが必要とするデータベース接続情報だけを割り当てます。
    # このように、サービスごとに必要な機密情報だけを安全に渡すことができます。
    secrets:
      - postgres_db
      - postgres_non_root_user
      - postgres_non_root_password

# =================================================================
# トップレベルの定義エリア
# =================================================================

# ここで定義したものが、各サービス内の`secrets:`セクションで参照されます。
secrets:
  # `postgres_user`という名前のsecretを定義します。
  postgres_user:
    file: ./secrets/postgres_user.txt # 実際の値は、このファイルから読み込まれます。
  # 同様に、他のsecretもファイルパスを指定して定義します。
  postgres_password:
    file: ./secrets/postgres_password.txGENERIC_TIMEZONE=Asia/Tokyo
TZ=Asia/Tokyo
 postgres_db:
    file: ./secrets/postgres_db.txt
  postgres_non_root_user:
    file: ./secrets/postgres_non_root_user.txt
  postgres_non_root_password:
    file: ./secrets/postgres_non_root_password.txt

# --- ボリュームの定義 ---
# サービス間で共有したり、データを永続化するためのボリュームを定義します。
volumes:
  db_storage:
  n8n_storage:

init-data.sh


#!/bin/bash
set -euo pipefail
# --- Docker Secretsから機密情報を読み込む ---
# docker compose secretsで設定された機密情報は、コンテナ内の `/run/secrets/` ディレクトリに
# ファイルとして自動的にマウント(配置)されます。
# `cat` コマンドでそのファイルの中身を読み取り、シェルの変数に代入しています。

POSTGRES_USER=$(cat /run/secrets/postgres_user)
POSTGRES_DB=$(cat /run/secrets/postgres_db)
POSTGRES_NON_ROOT_USER=$(cat /run/secrets/postgres_non_root_user)
POSTGRES_NON_ROOT_PASSWORD=$(cat /run/secrets/postgres_non_root_password)

# --- 以下は公式のコードのままで変更しない ---

if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
	psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
		CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
		GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
		GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
	EOSQL

	echo "SETUP INFO: Non-root user created: ${POSTGRES_NON_ROOT_USER}"
else
	echo "SETUP INFO: POSTGRES_NON_ROOT_USER or POSTGRES_NON_ROOT_PASSWORD not found. Skipping user creation."
fi

.env(例)

# n8n
GENERIC_TIMEZONE=Asia/Tokyo
TZ=Asia/Tokyo
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
DB_TYPE=postgresdb
POSTGRESDB_PORT=5432

「Azure CLI Container」で秘密情報を自動的に取得しよう

Docker Compose の Secrets 機能は tmpfs(メモリ上のファイルシステム) を利用して、コンテナの RAM 内に一時的なファイル領域を作成します。そこに秘密情報をファイルとして配置し、コンテナからは標準的に /run/secrets/ ディレクトリとして読み取れるようになります。この仕組みにより、秘密情報は次のように扱われます:

  • 読み取り専用ファイル としてマウントされるため、コンテナ内のアプリケーションは変更できません。
  • コンテナが停止または削除されると、tmpfs 領域は自動的にアンマウントされて消去されます。

その結果、Dockerの内部で機密データが残らず、セキュリティが向上します。

しかし、この仕組みでは秘密情報が依然としてホストのディスク上に保存されているため、外部からアクセスされるリスクが残っています。そこで、これらの課題を解決するため、秘密情報をクラウドから動的に取得する方法を考えます。

結論として、CLI 用コンテナで Secrets Management Service から機密情報を取得し、共有 Volume に配置する というアプローチを取ります。これにより、他のコンテナはその Volume を通じて機密情報を安全に利用できます。

Volume は tmpfs のようにコンテナ終了時に即削除されるわけではなく、Volumeはコンテナ終了後もホストのディスク上にデータが保持されますが、アクセスはすべてコンテナ内部で完結するため、.envファイルを直接配置する方式と比較してセキュリティは大幅に向上します。機密情報はあらかじめクラウドの Secrets Management Service に登録されているため、一元的に管理できるという利点もあります。

考えた仕組みは下記になります。

事前に用意する項目

今回の構築環境はAzureを利用するため、サービスを構築する前に3つの設定が必要です。

  • Azure Key Vaultで機密情報の設定
  • Azure VMにシステム割り当てマネージドIDを有効化
  • Azure Virtual MachineからAzure Key Vaultへの接続設定

Azure Key Vaultで機密情報の設定について

Azure Key VaultはMicrosoftが提供するクラウドベースのSecrets Management Serviceです。Azure Portalで「Azure Key Vault」を検索し、「Azure Key Vault」の管理画面を開きます。「Objects」の「Secrets」に今回のプロジェクトの秘密情報を追加します。今回は以下の5つの機密情報を追加しました。

「Enabled」項目を「Yes」に設定して、この機密情報を有効にします。

有効化後の機密情報画面は以下のようになります。

Azure VMにシステム割り当てマネージドIDを有効化

Azure VMのシステム割り当てマネージドIDは、仮想マシンに自動的に紐づけられるMicrosoft Entra ID上の認証IDです。システム割り当てマネージドIDを使用すると、パスワードや証明書などの認証情報を管理する必要がありません。Azureが認証情報を自動的に処理します。 一番簡単な方法はAzure Portalの管理画面でシステム割り当てマネージドIDを有効化します。

Virtual Machineの管理画面で、「Security」の「Identity」の項目で設定します。「System assigned」のタブの下で「Status」を「On」設定します。「Object ID」が付与されます。 システム割り当てマネージドIDが有効化されました。

Azure Virtual MachineからAzure Key Vaultへの接続設定について

Azure Key Vaultにアクセスするために、Azure RBACでマネージドIDに適切なロールを割り当てる必要もあります。

Azure Key VaultのRBACを有効化するには、「Azure Key Vault」の「Settings」から「Access configuration」で設定します。

「Azure role-based access control」を選択して、「Apply」をクリックしてRBACを適用します。

次は先にVMの「システム割り当てマネージドID」を有効化した「Identity」の画面に戻ります。「Azure role assignments」のボタンをクリックします。

今のサブスクリプションを選択します。

「Add role assignment 」のページで、Scopeを「Key Vault」に設定します。「Resource」をアクセスしたいAzure Key Vaultを選択します。「Role」を「Key Vault Secrets User」を選択します。

先の設定を保存したら、「Key Vault Secrets User」ロールが使用中のVMにアサインされた項目としてリストに表示されます。以上でAzure Virtual MachineからAzure Key Vaultへの接続設定は完了です。

実装してみる

Azure CLI ContainerでAzure Key Vaultから秘密情報を取得し、Volumeを通じて他のコンテナと共有する実装例は以下の通りです。APIキー、パスワード、証明書などの機密情報(シークレット)を安全に保存・管理するサービスを指します。

プロジェクトの構造は以下の通りです。

n8n
├── docker-compose.yml
├── Dockerfile
├── fetch_secret.sh
├── init-data.sh
└── .env

docker-compose.yml

version: '3.9'
services:

  # =======================================================
  # 1. 秘密情報取得サービス (secrets-fetcher)
  # 役割: Azure Key Vaultなどの安全な場所から秘密情報をダウンロードし、
  # 共有ボリューム (secrets_data) に書き込みます。
  # =======================================================
  secrets-fetcher:
    # 秘密情報取得スクリプトが含まれるDockerfileを、現在のディレクトリからビルドします。
    build: .
    # 'secrets_data'という共有ボリュームを、コンテナ内の'/secrets'ディレクトリにマウントします。
    # ダウンロードした秘密情報は、こVAULT_NAME_ENV=n8n-key-vaultボリューム内にファイルとして書き込まれます。
    env_file:
      - .env
    volumes:
      - secrets_data:/secrets
    # スクリプトが成功裏に実行され、秘密情報の書き込みが完了したら、このコンテナは停止します。
    # 継続的に実行する必要はありません。
    restart: "no"

  # =======================================================
  # 2. データベースサービス (postgres)
  # 役割: アプリケーションが使用するPostgreSQLデータベースです。
  # =======================================================
  postgres:
    image: postgres:16
    # 常に再起動するように設定します(コンテナがクラッシュした場合など)。
    restart: always
    environment:
      # Postgresは環境変数だけでなく、ファイルから認証情報を読み込むことができます。
      # ファイルのパスは、secrets-fetcherが書き込んだ共有ボリューム内の場所を指定します。
      POSTGRES_USER_FILE: /secrets/postgres_user.txt
      POSTGRES_PASSWORD_FILE: /secrets/postgres_password.txt
      POSTGRES_NON_ROOT_USER_FILE: /secrets/postgres_non_root_user2.txt
      POSTGRES_NON_ROOT_PASSWORD_FILE: /secrets/postgres_non_root_password.txt
      POSTGRES_DB: ${DB_TYPE}
      # このサービスを起動する前に、secrets-fetcherサービスが
      # 正常に終了するのを待ちます。
   depends_on:
     	  secrets-fetcher:
          condition: service_completed_successfully
    volumes:
      # データベースのデータを永続化するためのボリューム
      - db_storage:/var/lib/postgresql/data
      # データベース起動時に初期設定スクリプトを実行
      -./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
      # 秘密情報ファイルにアクセスするために、共有ボリュームをマウント
      - secrets_data:/secrets
    healthcheck:
      # データベースが正常に稼働しているかを確認するための設定
      # 'CMD-SHELL'でシェルコマンドを実行します。 # $$(cat /secrets/...) の部分で、秘密情報ファイルからユーザー名を読み込んでいます。
      test: ['CMD-SHELL', 'pg_isready -h localhost -U $$(cat /secrets/postgres_user.txt)  -d $$(cat /secrets/postgres_db.txt)']
      interval: 5s
      timeout: 5s
      retries: 10

  # =======================================================
  # 3. アプリケーションサービス (n8n)
  # 役割: ワークフロー自動化ツールn8nのアプリケーション本体です。
  # =======================================================
  n8n:
    # n8nの公式Dockerイメージを使用します。
    image: docker.n8n.io/n8nio/n8n
    # 常に再起動するように設定します。
    restart: always
    env_file:
      - .env
    environment:
      # .envファイルなどで定義された環境変数をコンテナに渡します
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${TZ}
      DB_TYPE: ${DB_TYPE}
      DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST}
      DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT}
      DB_POSTGRESDB_DATABASE: ${DB_POSTGRESDB_DATABASE}
      # データベース接続用のユーザー名/パスワードも、秘密情報ファイルから読み込みます。
      DB_POSTGRESDB_USER_FILE: /secrets/postgres_non_root_user.txt
      DB_POSTGRESDB_PASSWORD_FILE: /secrets/postgres_non_root_password.txt

    ports:
      - 5678:5678
    # レガシーなリンク機能ですが、ここではDB接続に使われます。
    volumes:
      # n8nのデータを永続化するためのボリューム
      - n8n_storage:/home/node/.n8n
      # 秘密情報ファイルにアクセスするために、共有ボリュームをマウント
      # ':ro' をつけることで、「読み取り専用 (Read-Only)」になり、安全性が高まります。
      - secrets_data:/secrets:ro
    depends_on:
      # このサービスを起動する前に、postgresサービスが
      # 正常な状態 (healthcheckが成功した状態) になるのを待ちます。
      postgres:
        condition: service_healthy

# =======================================================
# 共有ストレージ (ボリューム) の定義
# =======================================================
volumes:
  # データベースの永続化データ用
  db_storage:
  # アプリケーションの永続化データ用
  n8n_storage:
  # 秘密情報ファイル(パスワードなど)をサービス間で共有するためのボリューム
  secrets_data:

Dockerfile

FROM mcr.microsoft.com/azure-cli:latest # ベースイメージとして、Azure CLI (コマンドラインツール) が入った公式イメージを使用します。

WORKDIR /home # コンテナ内部の作業ディレクトリを /home に設定します。

COPY fetch_secret.sh . # ホスト側(あなたのPCがある場所)にある 'fetch_secret.sh' スクリプトを、コンテナ内の /home にコピーします。

RUN chmod +x fetch_secret.sh # コピーしたスクリプトファイルに実行権限を付与します。

# CMD (コマンド): コンテナが起動したときに実行されるメインの処理です。
CMD ["/bin/sh", "-c", "az login --identity && sh fetch_secret.sh"]
# 1. "az login --identity": Azure Managed Identity (管理されたID) を使ってAzureにログインします。
#    これにより、パスワードや認証情報を直接コンテナに渡さなくても、Key Vaultにアクセスできます。
# 2. "&&": ログインが成功したら、次のコマンドを実行します。
# 3. "sh fetch_secret.sh": コピーしておいた秘密情報取得スクリプトを実行します。
#    このスクリプトがKey Vaultから情報を取得し、共有ボリューム (/secrets) に書き込む役割を果たします。

fetch_secret.sh

#!/usr/bin/env bash
# シェルスクリプトの実行環境を指定します。
set -euo pipefail # スクリプトの実行設定。
# -e: コマンドが失敗(エラーコードが0以外)したら、即座にスクリプトを終了します。
# -u: 未定義の変数を使おうとしたら、エラーとして終了します。
# -o pipefail: パイプ(|)で繋いだコマンドの途中で失敗があった場合も、エラーとして終了します。(安全性向上)

#.envファイルが存在するか確認し、存在すればその内容を読み込む
if [ -f .env ]; then
  export $(grep -v '^#' .env | xargs)
fi

# 環境変数 VAULT_NAME_ENV から VAULT_NAME を設定する。
# もしVAULT_NAME_ENVが未設定または空の場合、デフォルトを使用する。
VAULT_NAME="${VAULT_NAME_ENV:-default-name}"

# VAULT_NAMEが最終的に空かチェックし、空の場合はエラーを表示して終了する。
if [ -z "$VAULT_NAME" ]; then
    echo "Error: VAULT_NAME is not set. Please set VAULT_NAME_ENV in the .env file or check the script's default value."
fi

# 秘密情報を書き込むディレクトリを変数に設定します。
# このパスは、docker-composeで設定した共有ボリューム(secrets_data)の場所です。
FOLDER="/secrets"
# 秘密情報書き込み用のディレクトリを、存在しなければ作成します。(-pオプションで途中のディレクトリもまとめて作成)
mkdir -p "$FOLDER"

# Key Vaultに保存されている全ての秘密情報(シークレット)の名前を取得し、ループで一つずつ処理します。
# az keyvault secret list: Key Vault内のシークレット一覧を取得するコマンド

for secret in $(az keyvault secret list \\
    --vault-name "$VAULT_NAME" \\
    --query "[].name" \\
    -o tsv); do

  # ループで取得した秘密情報名($secret)を使って、その秘密情報の「実際の値」を取得します。
  # az keyvault secret show: 特定のシークレットの詳細を取得するコマンド

  value=$(az keyvault secret show \\
    --vault-name "$VAULT_NAME" \\
    --name "$secret" \\
    --query "value" -o tsv)

  # ファイル名として安全に使えるように、秘密情報名に含まれるハイフン(-)をアンダースコア(_)に変換します。
  # 例: 'postgres-user' -> 'postgres_user'
  safe_name=$(printf '%s' "$secret" | tr '-' '_')
  # 秘密情報の値を書き出すファイルのパスを定義します。(例: /secrets/postgres_user.txt)
  FILE_PATH="$FOLDER/$safe_name.txt"

  # echo "Writing secret for: $safe_name to $FILE_PATH" # (動作確認用のコメントアウトされたログ出力)

  # 取得した秘密情報($value)を、対応するファイル($FILE_PATH)に書き込みます。
  # printf "%s" を使うことで、余分な改行や空白が入らず、秘密情報の値だけを正確にファイルに書き込めます。
  printf "%s" "$value" > "$FILE_PATH"
done

init-data.sh

#!/usr/bin/env bash
# このスクリプトはBashシェルで実行されます。
# エラーが発生した際に、すぐ終了するようにします。
set -euo pipefail
# --- 以下は公式のコードのままで変更しない ---
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
    psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
        CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
        GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTES_NON_ROOT_USER};
        GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
    EOSQL
else
    # 必要な環境変数が見つからない場合、ユーザー作成処理をスキップします。
    echo "SETUP INFO: POSTGRES_NON_ROOT_USER or POSTGRES_NON_ROOT_PASSWORD not found. Skipping user creation."
fi

.env(例)

GENERIC_TIMEZONE=Asia/Tokyo
TZ=Asia/Tokyo
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_TYPE=n8n
DB_POSTGRESDB_DATABASE=n8n
VAULT_NAME_ENV=n8n-key-vault  #アクセス先のKey vaultの名前を入力してください

この仕組みにより、"docker compose up -d"というワンコマンドで、VM上で秘密情報を直接扱うことなく、クラウドから秘密情報を取得し、コンテナを起動することができます。

おわりに

本記事では、.envファイルに潜むセキュリティリスクを起点に、Docker Compose Secrets、そしてAzure Key Vaultと連携した、より安全な機密情報管理の手法を段階的に解説しました 。

紹介した方法は、ソースコードを変更することなく導入でき、機密情報をリポジトリから完全に分離します。またはAzure Cliコンテナを用いてクラウドから動的にシークレットを取得する手法は、手作業によるミスの削減と、一元管理によるセキュリティ強化を両立させる強力な選択肢です 。

この機会により安全なアプリケーション運用の実現に繋げていただければ幸いです。

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

Tsai Yalin
Tsai Yalin
2023年8月メンバーズに中途入社。異なる言語や文化を体験するのが好きな人。
詳しく見る
ページトップへ戻る