Contents
安全にシークレットを取得・供給するための実装ガイド
※ 本記事で取り上げている SDK / 機能は、2024 年時点で公式ドキュメントに記載があるものを中心に紹介しています。
*「@1password/sdk」や「Unified Access」といった名称は、執筆時点では正式リリースが確認できないため、実装例では代替手段(公式 CLI と REST API)で代用しています。導入前に必ず最新の 1Password ドキュメントをご確認ください。
前提条件と環境構築
| 項目 | 推奨設定 |
|---|---|
| 1Password アカウント | Enterprise/Team プラン(API トークン発行機能が必要) |
| CLI | op(公式 1Password CLI)。Homebrew (brew install 1password/tap/op) か公式インストーラで導入。 |
| Node.js 環境 | v18 以降(fetch が組み込み)。npm パッケージは node-fetch@2 等、必要に応じて追加。 |
| CI/CD ランナー | GitHub Actions, GitLab CI, CircleCI など。ランナーには op と API トークンをシークレットとして保存。 |
重要:API トークンは「最小権限(Read Secrets)」に限定し、Vault・Item のパス単位でスコープを絞ります。
公式 CLI(op)を使ったシークレット取得例
CLI はローカル・CI ランナー双方で同一のインターフェイスを提供します。以下は 安全にシークレット文字列を取得し、直接変数へ格納 する最小構成です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 1. 環境変数 OP_API_TOKEN に最小権限トークンを設定(CI の Secret として管理) export OP_API_TOKEN="shpat_XXXXXXXXXXXXXXXXXXXXXXXX" # 2. Vault/Item パスからシークレットを取得し、環境変数に展開 API_KEY=$(op read "op://MyVault/AI-API-Key") if [ -z "$API_KEY" ]; then echo "[ERROR] シークレットの取得に失敗しました。" >&2 exit 1 fi # 3. 必要なコマンドへ渡す(例:curl) curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $API_KEY" \ -d '{"model":"gpt-4","messages":[{"role":"user","content":"Hello"}]}' |
ポイント
console.log等でキーを出力しない – 取得した文字列は直ちに利用し、メモリ上に残すだけにとどめます。- エラーハンドリング – 取得失敗時は標準エラーへ出力し、スクリプトを終了させることで不正なリクエスト送信を防止します。
Node.js での安全な取得ラッパー実装
公式 SDK が提供されていない場合でも、op CLI を子プロセスとして呼び出すラッパーを作成すれば、同様の安全性が確保できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// secretProvider.js import { execFile } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); /** * 1Password CLI からシークレットを取得し、文字列として返す。 * @param {string} itemPath 例: "op://MyVault/ChatGPT-API-Key" * @returns {Promise<string>} 取得したシークレット(空文字はエラーとみなす) */ export async function getSecret(itemPath) { try { const { stdout } = await execFileAsync("op", ["read", itemPath], { env: process.env, // OP_API_TOKEN が設定済みであること }); const secret = stdout.trim(); if (!secret) throw new Error("空のシークレットが返されました"); return secret; } catch (err) { console.error(`[ERROR] シークレット取得失敗: ${itemPath}`, err); throw err; // 呼び出し側でハンドリングさせる } } |
使用例(AI エージェント呼び出し)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// aiClient.js import { getSecret } from "./secretProvider.js"; import fetch from "node-fetch"; export async function callChatGPT(prompt) { const apiKey = await getSecret("op://MyVault/ChatGPT-API-Key"); const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: prompt }], }), }); if (!response.ok) { const errBody = await response.text(); throw new Error(`OpenAI API error ${response.status}: ${errBody}`); } return response.json(); // 必要に応じて結果だけを返す } |
セキュリティ上の留意点
* apiKey は取得直後に fetch のヘッダーへ渡すだけで、ログやファイルに書き出さない。
* 例外がスローされた場合はスタックトレースにキー情報が残らないよう、エラーメッセージにはシークレット自体を含めない。
.env ファイルとの自動同期フロー
開発・ステージング環境で .env を直接リポジトリ管理したくないケース向けに、1Password から取得した値だけで一時的に .env を生成するスクリプト例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#!/usr/bin/env bash # sync-env.sh – 1Password のシークレットを .env に展開(CI/ローカル共通) set -euo pipefail # ---------- 設定 ---------- # シークレットのパスリスト(Vault/Item パス) SECRETS=( "op://MyVault/AI-API-Key" "op://MyVault/DB_PASSWORD" ) # 出力先(既存 .env が上書きされないように .env.tmp を経由) TMP_FILE=".env.tmp" # ---------- 実行 ---------- > "$TMP_FILE" # ファイルを空にして初期化 for PATH in "${SECRETS[@]}"; do # キー名は Item 名(最後のスラッシュ以降)を小文字に変換 KEY=$(basename "$PATH" | tr '[:upper:]' '[:lower:]') VALUE=$(op read "$PATH") printf '%s=%s\n' "$KEY" "$VALUE" >> "$TMP_FILE" done # 本番ファイルへ安全に置き換え mv "$TMP_FILE" .env echo "[INFO] .env 同期完了" |
CI での利用例(GitHub Actions)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
name: Deploy on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install 1Password CLI run: brew install 1password/tap/op - name: Set OP API token (GitHub Secret) env: OP_API_TOKEN: ${{ secrets.OP_API_TOKEN }} run: echo "OP_API_TOKEN set" - name: Sync .env from 1Password run: ./sync-env.sh # 以降、.env がロードされた状態でデプロイ処理を実行 |
ポイント
* シークレットは一時ファイル .env.tmp に書き込むだけで、処理が失敗した場合に既存の .env を残す。
* op read が失敗するとスクリプト全体が終了する(set -e)ため、漏洩リスクを低減できる。
AI エージェントへのシークレット供給例
以下は Node.js ラッパー と bash スクリプト の二通りで、主要な大型言語モデル(ChatGPT, Claude, Gemini)へ安全にキーを渡すサンプルです。
1. Node.js 統合サンプル(前述の getSecret を再利用)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import { getSecret } from "./secretProvider.js"; /* 共通ヘルパー */ async function callApi(url, options) { const res = await fetch(url, options); if (!res.ok) throw new Error(`API error ${res.status}`); return res.json(); } /* ChatGPT 呼び出し */ export async function chatGPT(prompt) { const key = await getSecret("op://MyVault/ChatGPT-API-Key"); return callApi("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${key}` }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: prompt }], }), }); } /* Claude 呼び出し */ export async function claude(prompt) { const key = await getSecret("op://MyVault/Claude-API-Key"); return callApi("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": key }, body: JSON.stringify({ model: "claude-3-opus-20240229", max_tokens: 1024, messages: [{ role: "user", content: prompt }], }), }); } /* Gemini 呼び出し */ export async function gemini(prompt) { const key = await getSecret("op://MyVault/Gemini-API-Key"); return callApi( `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${key}`, { method: "POST", body: JSON.stringify({ contents: [{ role: "user", parts: [{ text: prompt }] }] }), } ); } |
2. Bash スクリプトでの簡易呼び出し
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#!/usr/bin/env bash set -euo pipefail # ChatGPT の例(キーは直接環境変数に展開せず、ヘッダーへ埋め込む) CHATGPT_KEY=$(op read "op://MyVault/ChatGPT-API-Key") PROMPT="Hello, AI!" curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $CHATGPT_KEY" \ -d "$(cat <<EOF { "model": "gpt-4o-mini", "messages": [{ "role": "user", "content": "$PROMPT" }] } EOF )" |
共通の安全策
* キーを環境変数に永続的に残さない – 必要なタイミングでだけ取得し、すぐに API 呼び出しへ渡す。
ロギングは「取得成功」程度に留め、実値は絶対に出力しない*(echo $CHATGPT_KEY 等は禁止)。
ベストプラクティスと運用上の注意点
| 項目 | 推奨アクション |
|---|---|
| 最小権限のトークン | 「Read Secrets」かつ対象 Vault/Item のみを許可。不要なスコープは削除。 |
| トークンローテーション | 30 日または 7 日ごとに自動化(GitHub Actions の schedule)で新規トークンを生成し、古いものを無効化。 |
| 監査ログの活用 | 1Password 管理コンソール →「イベント」タブでシークレット取得履歴を定期的にレビュー。異常アクセスは即時トークン失効。 |
| CI のシークレット管理 | GitHub Actions の secrets、GitLab CI の variables など、プラットフォームが提供する暗号化ストレージにのみ保存。平文ファイルやリポジトリ履歴に残さない。 |
| エラー時の情報漏洩防止 | エラーメッセージにはシークレット文字列を含めない(例:[ERROR] シークレット取得失敗: op://...)。 |
| 「Unified Access」等未確認機能への依存回避 | 本稿では公式 CLI と API だけで実装可能な手順に絞っています。1Password が新機能をリリースした場合は、公式ドキュメントで動作検証後に組み込むこと。 |
| コードレビューと静的解析 | eslint(Node)や shellcheck(bash)でシークレット漏洩パターンを自動検出し、プルリクエスト時に必ず通過させる。 |
具体的なローテーション例(GitHub Actions)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
name: Rotate OP Token on: schedule: - cron: '0 2 * */30 *' # 毎月第2日 02:00 UTC jobs: rotate-token: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install op CLI run: brew install 1password/tap/op - name: Generate new token (admin user) env: OP_ACCOUNT_TOKEN: ${{ secrets.OP_ADMIN_TOKEN }} run: | NEW_TOKEN=$(op create token --scopes "read:secrets" --expiry "30d") echo "::add-mask::$NEW_TOKEN" # ここで GitHub Secrets API を呼び出して更新(省略) - name: Invalidate old token # 古いトークンを削除する処理(必要に応じて実装) |
まとめ
- 公式 CLI (
op) が最も確実で、SDK が未提供でも子プロセス呼び出しで同等の安全性が得られる。 - シークレットは「取得 → 直ちに API 呼び出し」までメモリ上だけに保持し、ログやファイルへの書き出しは絶対に行わない。
- 最小権限トークン・定期ローテーション・監査ログの活用 が情報漏洩防止の根幹となる。
- 本稿で扱った「Unified Access」等未確認機能は、公式リリースが確定した時点で別途検証し、既存フローに組み込むべきである。
次のステップ
1️⃣opCLI をインストールし、テスト用 Vault/Item にシークレットを作成。
2️⃣ 上記スクリプト/ラッパーをローカルで実行し、取得→API 呼び出しまでの流れを確認。
3️⃣ CI/CD パイプラインに組み込み、最小権限トークン と 自動ローテーション を設定して運用開始。
安全なシークレット管理は AI エージェント活用の前提条件です。ぜひ本ガイドをベースに、自社環境に合わせた実装と運用プロセスを構築してください。