1台のVPSに最適なセルフホスト型シークレット管理
1台のVPSならOpenBao、Infisical、SOPSとage、systemd credentials、mode 600のenvファイルのどれを選ぶべきか、運用負担と費用を比較します。
パスワードマネージャーではできない、セルフホスト型シークレットマネージャーの役割
セルフホスト型シークレットマネージャーは、認証情報をプロセスに渡します。パスワードマネージャーは、認証情報を人に渡します。その他の違いは、すべてこの違いから生じます。パスワードマネージャーは、その場にいて操作している人がロックを解除します。シークレットマネージャーは、誰も起きていない 03:00 に、アプリケーションへデータベースのパスワードを渡さなければなりません。
障害の形態も異なります。この違いは運用上重要です。ロックされたパスワードマネージャーは不便なだけです。マスターパスワードをもう一度入力すれば済みます。封印されたシークレットマネージャーは停止障害になります。封印中に再起動したすべてのサービスは、認証情報なしで起動し、そのまま停止します。自分用のパスワードマネージャーとして Vaultwarden を運用することで、人に関する問題は適切に解決できます。しかし、マシンに関する問題は解決できません。そもそも、そのために作られたものではありません。これと併用する場合、強化すべき対象は、クライアントがすでに暗号化している vault の内容ではなく、管理者トークンとバックアップファイルです。Vaultwarden のセキュリティ強化で、この両方を扱えます。
1 台のサーバーで現実的な選択肢は、2 つのグループに分かれます。OpenBao と Infisical はサービスです。API、データベース、TLS (transport layer security)、ログイン処理、そして稼働状態を維持するプロセスが必要になります。SOPS と age、systemd credentials、Docker secrets はファイルです。保存時は暗号化され、すでに稼働している何かが復号します。追加で監視するものはありません。
先に率直な結論を述べます。1 台のサーバーを 1 人か 2 人で使う場合、通常はファイルベースの選択肢が適切です。適切に unseal されず、ローテーションも行われない OpenBao は、mode 600 の env ファイルより悪い選択です。可動部分と、誤った方法で取得することになるバックアップが増える一方で、手作業で実施していなかったローテーション機能は得られないためです。
mode 600 の環境変数ファイルで十分ですか?
多くの場合は十分です。防ぐ脅威は、同じサーバー上の別ユーザーがデータベースのパスワードを読み取ることです。Unix のファイル権限でこれを防げます。ネットワークが起動する前にも機能します。
sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 640 -o root -g myapp /dev/null /etc/myapp/env
sudoedit /etc/myapp/env両方の立場から確認します。
sudo -u myapp cat /etc/myapp/env
sudo -u nobody cat /etc/myapp/env1 つ目のコマンドはファイルを表示します。2 つ目のコマンドは cat: /etc/myapp/env: Permission denied を表示します。nobody は myapp グループに属しておらず、ファイルにも他ユーザー向けの権限がないためです。これがセキュリティモデルのすべてです。実際に有効な対策です。
問題は、その後に発生する情報漏えいです。EnvironmentFile= を指定した systemd unit は、これらの値をプロセス環境にコピーします。プロセス環境は読み取り可能です。
[Service]
User=myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/usr/local/bin/myappsudo cat /proc/$(pgrep -n myapp)/environ | tr '\0' '\n'/proc/<pid>/environ は root とプロセスの実行ユーザーが読み取れるため、このコマンドは Secret を平文で表示します。クラッシュレポーターが環境変数をレポートに添付する場合も、同じ情報を取得できます。同じアカウントで実行されるツールも同様です。そのため、AI エージェントから Secret を除外することは、まず環境変数から Secret を除外することから始まります。ファイルを 権限を最小限にした専用サービスユーザーと組み合わせ、「プロセスの実行ユーザー」が root にならないようにします。
age による SOPS: Git にコミットできる暗号化 Secret
SOPS (secrets operations) は、YAML または JSON ファイルの値を暗号化し、キーは平文のまま残します。age は、小さな暗号化ツールです。1 組のキーペアだけを使用し、キーディレクトリは必要ありません。これらを組み合わせると、secrets.enc.yaml をコードと一緒にコミットできます。また、git diff によって、読み手に変更後の値を知らせずに、どの設定が変更されたかを確認できます。
age は Ubuntu 24.04 にパッケージされています。SOPS はパッケージされていないため、リリースページから .deb を取得します。2026 年 8 月時点の現行バージョンは 3.13.3 です。
sudo apt update && sudo apt install -y age
curl -LO https://github.com/getsops/sops/releases/download/v3.13.3/sops_3.13.3_amd64.deb
sudo apt install -y ./sops_3.13.3_amd64.deb
sops --versionキーペアを生成します。age-keygen は秘密鍵をファイルに書き込み、公開鍵を出力します。そのため、Public key: age1... で始まる行が表示されます。
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
age-keygen -y ~/.config/sops/age/keys.txt公開鍵をリポジトリのルートにある .sops.yaml に記述します。これにより、コマンドラインで recipient を指定する必要がなくなります。
creation_rules:
- age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23csops encrypt secrets.yaml > secrets.enc.yaml
sops decrypt secrets.enc.yamlpath_regex のないルールはすべてに一致します。最初はこの設定で問題ありません。後から追加する場合は、sops に渡すファイルに一致するように記述してください。ルールは出力をリダイレクトする先のファイルではなく、入力パスに対して評価されるためです。
実行時は、値を 1 つのプロセスだけに渡します。
sops exec-env secrets.enc.yaml './myapp'sops exec-env はメモリ上で復号し、子プロセスの環境変数に値を設定します。そのため、平文がディスクに書き込まれることはありません。前のセクションで説明した環境変数に関する注意点は、その子プロセスにも適用されます。
ここでは 2 つの点で問題が起きやすくなります。systemd で Failed to get the data key required to decrypt the SOPS file というエラーが出る場合、ほとんどの場合、SOPS が誤ったホームディレクトリを参照しています。unit は HOME を引き継がないためです。unit の Environment=SOPS_AGE_KEY_FILE=/etc/sops/age.txt でパスを明示的に設定してください。もう 1 つは、.sops.yaml を編集しても、すでに存在するファイルは再暗号化されないことです。同僚の公開鍵を追加しても新しいファイルにしか適用されないため、既存の各ファイルに対して sops updatekeys secrets.enc.yaml を実行してください。設定をすでに Ansible で管理している場合は、同じ値を Ansible Vault で暗号化することで、別のツールを追加せずに同じ結果を得られます。
systemd credentials: 環境変数に到達しないシークレット
Ubuntu 24.04 には systemd 255 が含まれているため、インストールは不要です。systemd-creds はホスト上でシークレットを暗号化し、systemd はそのシークレットを、対象サービスだけが読み取れる非公開ディレクトリに復号します。
sudo systemd-creds setup
sudo install -d -m 700 /etc/myapp
echo -n 'hunter2' | sudo systemd-creds encrypt --name=db_password - /etc/myapp/db_password.cred[Service]
User=myapp
LoadCredentialEncrypted=db_password:/etc/myapp/db_password.cred
ExecStart=/usr/local/bin/myappサービスは、$CREDENTIALS_DIRECTORY が示すディレクトリ内の db_password というファイルから値を読み取ります。値は環境変数に含まれないため、/proc/<pid>/environ を実行しても有用な情報は表示されません。また、平文が root ファイルシステムに書き込まれることもありません。
unit から参照する前に、ファイルを復号できることを確認します。
sudo systemd-creds decrypt /etc/myapp/db_password.cred -どの鍵で暗号化したかを把握してください。バックアップが役立つかどうかは、それによって決まります。デフォルトの --with-key=auto は、利用可能で使用できる場合は TPM2(trusted platform module version 2)チップを使用し、それ以外の場合はホスト鍵を使用します。ほとんどの VPS インスタンスには TPM2 がありません。
systemd-analyze has-tpm2no はホスト鍵が使用されたことを示します。この鍵は /var/lib/systemd/credential.secret に保存され、root だけが読み取れます。db_password.cred をそのファイルがない新しい VPS に復元しても、復号できません。credential.secret も同じバックアップに含めるか、平文を引き続きアクセスできる場所に保管してください。
Docker secrets: /run/secrets 配下のファイル
Compose はホスト上のファイルを読み込み、/run/secrets/<name> にコンテナへマウントします。
services:
app:
image: myapp:latest
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtdocker compose exec app cat /run/secrets/db_password
docker compose exec app env | grep -i password1つ目のコマンドは Secret の内容を出力します。2つ目は DB_PASSWORD_FILE=/run/secrets/db_password だけを出力します。ここが重要です。値はコンテナの環境変数に入らないため、docker inspect の出力にも現れません。多くの公式イメージはこの形式を前提としており、Postgres イメージも POSTGRES_PASSWORD_FILE をこの方法で読み取ります。
この仕組みの意味を明確にしておきます。Swarm mode の外部では、どの層でも暗号化されません。./db_password.txt はホスト上の平文ファイルであり、保護手段はファイルのモードと所有者だけです。これらは自分で設定してください。Compose は、警告なしに誰でも読み取れるファイルをマウントするためです。通常の env_file の簡易的な方法との幅広いトレードオフについては、Compose の env ファイルと Secret に関するガイドを参照してください。
OpenBao と Vault の実際の運用コスト
OpenBao は HashiCorp Vault の Linux Foundation フォークです。HashiCorp が 2023 年に Vault のライセンスを Business Source License に変更した後に始まりました。OpenBao は MPL 2.0 (Mozilla Public License) を維持しています。2026 年 8 月時点の最新リリースは 2.6.2 です。フォーク後もコマンド体系の大部分が維持されているため、以下のほぼすべての内容は Vault にも当てはまります。
docker pull docker.io/openbao/openbaoアップグレードを apt で管理したい場合は、OpenBao のダウンロードページから Debian および Ubuntu 用パッケージを入手できます。サーバーには、リスナーとストレージバックエンドを記述した設定ファイルが必要です。
listener "tcp" {
address = "127.0.0.1:8200"
tls_cert_file = "/path/to/full-chain.pem"
tls_key_file = "/path/to/private-key.pem"
}
storage "raft" {
path = "/path/to/raft/data"
node_id = "raft_node_1"
}次に、1 回起動します。
bao operator initデフォルトでは、root key が 5 個の share に分割され、そのうち 3 個を指定すると unseal できます。これらが -key-shares フラグと -key-threshold フラグです。share と初期 root token は 1 回だけ表示され、再表示されることはありません。
ここからが、多くの比較記事で省略される点です。再起動したサーバーは sealed 状態です。 OpenBao は root key をメモリにだけ保持するため、再起動後は share のしきい値を誰かが入力するまで、自身のストレージを復号できません。そのため、カーネル更新や out of memory による kill の後には、sealed 状態のサーバーと、ログインできないアプリケーションが残ります。
1 人で運用する VPS では、Shamir split による保護はありません。5 個すべての share が、同じ人に属する同じ password manager に保存されるためです。Auto unseal を使うと、key は信頼できるデバイスまたはサービスに移されます。大規模な cloud では managed key service が一般的ですが、VPS では通常、保護対象のデータと同じディスク上に key file を置くことになります。これは、再起動後にサーバーが自動的に復旧する代わりに、セキュリティを実際に低下させます。トレードオフを理解したうえで選択し、どちらを選んだか記録してください。
Infisical: UI、データベース、そして手元で保持し続ける master key
Infisical は、Web インターフェース、プロジェクト、環境、ユーザー単位のアクセス制御を備えた secrets プラットフォームです。Compose でセルフホストする手順は簡単です。
curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml
curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example
docker compose -f docker-compose.prod.yml up -d最後のコマンドを実行する前に .env を編集します。2 つの値は自分で設定する必要があり、そのうち 1 つは以後変更してはいけません。
openssl rand -hex 16
openssl rand -base64 321 つ目は ENCRYPTION_KEY です。これは 16 byte の hex 文字列です。PostgreSQL 内で secrets を暗号化する key なので、これを失うと完全なデータベースバックアップも ciphertext の集合になり、稼働中のインスタンスで変更すると既存の secrets を復号できなくなります。2 つ目は AUTH_SECRET です。これはセッションに使用する 32 byte の base64 文字列です。SITE_URL には、実際にアクセスする絶対 URL をプロトコル込みで指定する必要があります。指定を誤るとログイン後のリダイレクトが失敗します。
実際に必要なのが人による運用であれば、OpenBao より Infisical のほうが適しています。小規模チーム向けの Web インターフェースと環境間の分離を利用できるためです。これは、自然に期限切れになるデータベース credentials を必要とする場合とは異なります。その代わり、PostgreSQL、Redis、TLS 証明書が必要になり、これらも自分で patch とバックアップを行う必要があります。
シークレットサービスが停止中にアプリケーションを再起動するとどうなるか
この問題は、シークレットサービスを単一のホストで運用できるかどうかを判断します。ネットワークが起動する前でもファイルは読み取れます。サービスは読み取れません。
ホストを再起動すると、アプリケーションと OpenBao が同時に起動します。アプリケーションがデータベースのパスワードを要求した時点で、OpenBao はまだ封印状態です。要求は失敗し、systemd は人が unseal share を入力するまでアプリケーションをループ状に再起動します。何かが壊れているわけではありません。ただし、何も起動した状態にはなりません。
この問題には、現実的な対処方法が2つあります。unit の起動順序を設定し、アプリケーションに再試行させます。After= シークレットサービスに加えて、API を過剰に呼び出さないよう十分に長い Restart=on-failure と RestartSec= を設定します。もう1つは、起動時ではなくデプロイ時に取得する方法です。シークレットを mode 600 のファイルまたは systemd credential に展開すれば、実行中のシステムが依存する対象は API ではなくファイルになります。
トークンの有効期限切れも、時間の進み方が遅い同じ問題です。OpenBao のトークンとリースには有効期間があるため、更新しない長時間稼働プロセスは、デプロイとは無関係な時点でアクセスを失います。その日に何も変更していないため、この障害は特に分かりにくくなります。
ストア自体のバックアップ
ここで扱う各方式には鍵があり、その鍵がなければバックアップは役に立ちません。鍵の保管場所を記録してください。
env ファイルを使う場合は、ファイル自体が Secret であるため、バックアップを暗号化する必要があります。SOPS では、暗号化済みファイルは公開されている場所に置けますが、~/.config/sops/age/keys.txt にある age private key は絶対に失ってはいけません。systemd credentials では、.cred ファイルと一緒に /var/lib/systemd/credential.secret をバックアップしてください。Infisical では PostgreSQL dump を取得し、ENCRYPTION_KEY を dump とは別の場所に保存してください。
OpenBao で raft storage を使う場合は、専用の snapshot を取得します。
bao operator raft snapshot save backup.snap
bao operator raft snapshot restore backup.snapsnapshot には暗号化された storage が含まれます。そのため、新しいサーバーに復元する場合も bao operator init の unseal shares が必要です。shares をどこにも保存せず、毎晩 snapshot だけを object storage にコピーするジョブは、何もバックアップしていません。実際に依存する前に、使い捨ての VPS で復元をテストしてください。
監査ログ: どのユーザーがどの Secret を読み取ったか
ファイルだけでは監査証跡を残せません。モードと所有者から分かるのは、誰が Secret を読み取れる可能性があるかだけです。実際に誰が読み取ったかは分かりません。auditd でパスを監視する方法が最も近い代替手段ですが、報告できるのはファイルが開かれたことまでで、どの値が使用されたかは分かりません。
OpenBao は、明示的に有効化した監査デバイスへすべてのリクエストを記録します。
bao audit enable file file_path=/var/log/openbao_audit.logこのログには、サーバーの運用方法を変える重要な点が2つあります。リクエストとレスポンスに含まれる文字列の多くは、salt を使用した HMAC-SHA256 でハッシュ化されます。そのため、ログ自体に平文を残さず、既知の値とログ内の値を照合できます。整数と boolean は平文で記録されるため、数値の Secret はこのハッシュによる保護を受けません。
さらに、運用上の落とし穴があります。有効な監査デバイスがリクエストを記録できない場合、OpenBao はリクエストに応答しません。デバイスがブロッキング状態で失敗すると、誰かが修復するまでリクエストがハングします。/var/log のディスクが満杯になると、設計上、Secret API は停止します。最初の障害が発生してからではなく、初日に監査ログ専用の領域と logrotate のルールを用意してください。
どの self-hosted secrets manager を運用すべきか
マシンの台数と利用者の人数を数えてから選びます。
- 1 台のマシンを 1 人で使う場合: root が所有し、サービスユーザーが読み取る mode 600 の環境変数ファイルを使います。値をプロセス環境から分離したい場合は、systemd credentials を追加します。
- 1 台のマシンを 2〜5 人で使い、設定をすでに git で管理している場合: SOPS と age を使います。各利用者に key pair を割り当て、
.sops.yamlに復号を許可するすべての公開鍵を記載します。 - 複数のマシンで 1 つの設定リポジトリを使い、有効期限付きの credentials が不要な場合: 引き続き SOPS と age を使います。各ホストに 1 つの recipient key を割り当てるため、ホストキーが盗まれても、そのホストのファイルだけを復号できます。
- 複数のマシンと複数のチームがあり、有効期間付きのデータベース credentials と、実際に確認される監査証跡が必要な場合: OpenBao を使います。unseal とリストア訓練のため、運用担当者の作業時間を月 1 時間、予算に組み込みます。
4 つすべてに共通する原則は同じです。口頭で説明できる要件を満たす最小のものを運用します。停止している secrets manager は、空の secrets manager と区別できないためです。
FAQ
シングル VPS で自己ホスト型の Secret 管理サービスを使う価値はありますか?
通常はありません。OpenBao や Infisical のようなサービスを指している場合は特にそうです。1 台のサーバーを 1 人か 2 人で使うなら、mode 600 の環境変数ファイルまたは systemd の暗号化 credential で、別のローカルユーザーに対して同等の保護を実現できます。unseal の手順も、追加でパッチを適用するサービスも不要です。複数のマシンと複数のユーザーがある場合や、誰かが手動でローテーションしなくても期限切れになる credential が本当に必要な場合は、Secret サービスを導入するメリットが出てきます。
password manager と secrets manager の違いは何ですか?
password manager は、人が入力する credential を保存し、利用者がその場でロックを解除します。一方、secrets manager はプロセスに credential を渡すため、誰も監視していない 03:00 でも動作しなければなりません。違いはこの点にあります。ロックされた password manager では master password の再入力が必要ですが、sealed 状態の secrets manager では、seal 中に再起動したすべてのサービスが停止します。
再起動後に OpenBao が sealed になると、アプリには何が起こりますか?
アプリは Secret を取得できないため起動に失敗します。その後、誰かが unseal threshold を満たすまで systemd がループ状態で再起動します。デフォルトでは、5 個の share のうち 3 個が必要です。OpenBao は root key をメモリ内だけに保持するため、再起動するたびに再び seal されます。auto unseal を有効にする方法があります。ただし、単一 VPS では unseal key がデータと同じディスクに保存されることになります。別の方法として、deploy 時に Secret をファイルへ出力し、起動が API に依存しないようにします。
SOPS で暗号化したファイルを公開リポジトリに commit できますか?
値は暗号化されているため、age private key を持たない人からは保護されます。ただし、key 自体は暗号化されません。読み手は、あなたが STRIPE_SECRET_KEY と SMTP_PASSWORD を保持していることや、それぞれが変更される頻度を確認できます。このメタデータは、ほとんどのプロジェクトでは許容できますが、一部のプロジェクトでは許容できません。age private key はリポジトリの外で管理し、recipient を追加または削除するたびに、既存のすべてのファイルに対して sops updatekeys を実行してください。