← AI開発 資料アーカイブ
システム構築ガイド

Claudeスキル限定配布のセキュリティ設定手順書(Cloudflare)

元ファイル: SECURITY_SETUP_GUIDE.md

要約

Claude Codeスキルを限定配布する際のセキュリティ構成手順書。①Cloudflare Pagesによるサイトパスワード、②Cloudflare Worker+KVによるアクティベーションキー認証の2段階構成で、スキルの機密指示を漏らさず配布する。フォルダの3ゾーン分離、デプロイフロー、運用コマンド、デプロイ前チェックリストまでを網羅する。

要点

セキュリティCloudflareWorkerKVスキル配布アクティベーションキー認証

スキル配布セキュリティ設定 手順書

対象: Claude Code スキルを限定配布する際のセキュリティ設定 構成: ① サイトパスワード(Cloudflare Pages)+ ② アクティベーションキー(Cloudflare Worker + KV)


Part 0: フォルダ構造の設計(作業開始前に必ず整理する)

3つのゾーンに分ける

your-project/               ← 開発フォルダ全体(自分のPC)
│
├── cloudflare/             ← ② Cloudflare にアップするもの
│   ├── worker.js           ←   スキル指示(秘密)+ 認証API
│   ├── wrangler.toml       ←   Worker設定
│   └── pages/              ←   ダウンロードページ
│       ├── index.html
│       ├── _headers
│       ├── functions/
│       │   └── _middleware.js
│       └── your-skill.zip  ←   配布ZIPをここに置いてデプロイ
│
├── dist/                   ← ① ユーザーに配布するもの(ZIPの元)
│   └── your-skill/         ←   これをZIP圧縮して cloudflare/pages/ に置く
│       ├── activate.key    ←   PASTE-YOUR-KEY-HERE のまま
│       ├── .env.example    ←   APIキー記入テンプレート
│       ├── .claude/
│       │   └── commands/
│       │       └── skill-name.md  ← 認証シェルのみ(指示は含まない)
│       └── skill/
│           └── skill-name/
│               └── scripts/      ← 実行スクリプト
│
├── skill/                  ← 開発用スキル本体(配布しない)
│   └── skill-name/
│       └── SKILL.md        ← フル指示(機密)
│
├── output/                 ← 生成物(配布しない)
│   └── articles/
│
└── docs/                   ← ドキュメント(配布しない)

各ゾーンの役割

ゾーン アップ先 内容
cloudflare/ Cloudflare(Worker + Pages) 秘密のスキル指示・認証API・ダウンロードページ
dist/ ZIPにしてCloudflare Pagesに置く ユーザーへの配布物(指示は含まない殻だけ)
skill/ どこにもアップしない スキルの本体(機密)
output/ どこにもアップしない 生成した記事・画像

Gitにコミットしてはいけないもの(.gitignore

.env
activate.key
output/
skill/*/scripts/__pycache__/
skill/*/.venv/
*.key

dist フォルダ → ZIP → Pages へのフロー

① dist/your-skill/ の中身を確認・整理
        ↓
② ZIPに圧縮
   (Windows PowerShell)
   Compress-Archive -Path "dist\your-skill" -DestinationPath "dist\your-skill-v1.0.zip"
        ↓
③ ZIPを cloudflare/pages/ にコピー
   cp dist/your-skill-v1.0.zip cloudflare/pages/
        ↓
④ Cloudflare Pages にデプロイ
   cd cloudflare/pages
   npx wrangler pages deploy . --project-name [プロジェクト名] --commit-dirty=true

dist フォルダに入れるもの / 入れないもの

ファイル dist に含める 理由
activate.key(PASTE-YOUR-KEY-HERE) ユーザーがキーを入力する場所
.env.example APIキー設定の案内
.claude/commands/skill-name.md 認証シェル(指示なし)
skill/scripts/ 実行に必要なスクリプト
skill/SKILL.md(フル指示) 機密。Workerに保管
.env(実際のAPIキー) 絶対に含めない
output/ 生成物。不要
.venv/ 容量大。ユーザーが自動生成

全体構成図

ユーザー
  │
  ├─ ① ダウンロードページにアクセス
  │     → Cloudflare Pages ミドルウェアがパスワードを要求
  │     → パスワードOK → ZIPをダウンロード
  │
  └─ ② スキルを実行(/スキル名)
        → activate.key を読む
        → Cloudflare Worker に問い合わせ
        → KV でキーの有効性を確認
        → 有効 → スキル指示を返す → 実行開始
        → 無効 → エラーで停止

前提条件


Part 1: サイトパスワード設定(Cloudflare Pages)

1-1. フォルダ構成を作る

your-project/
└── cloudflare/
    └── pages/
        ├── index.html          ← ダウンロードページ
        ├── _headers            ← ZIPダウンロード用ヘッダー
        ├── your-skill.zip      ← 配布ファイル
        └── functions/
            └── _middleware.js  ← パスワードゲート

1-2. functions/_middleware.js を作成

/**
 * Password Gate Middleware
 * Cloudflare Pages Functions
 * 環境変数 SITE_PASSWORD にパスワードを設定する。
 */

const COOKIE_NAME = 'auth_token';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7日間

export async function onRequest(context) {
  const { request, env, next } = context;
  const PASSWORD = (env.SITE_PASSWORD || '').trim();
  const url = new URL(request.url);

  // POST: パスワード送信処理
  if (request.method === 'POST' && url.pathname === '/__auth') {
    const formData = await request.formData();
    const input = formData.get('password') || '';

    if (input === PASSWORD) {
      const redirectTo = formData.get('redirect') || '/';
      const headers = new Headers({ Location: redirectTo });
      headers.append('Set-Cookie',
        `${COOKIE_NAME}=${PASSWORD}; Path=/; Max-Age=${COOKIE_MAX_AGE}; HttpOnly; Secure; SameSite=Lax`
      );
      return new Response(null, { status: 302, headers });
    }

    return new Response(renderPage(true, url.pathname), {
      status: 401,
      headers: { 'Content-Type': 'text/html; charset=UTF-8' }
    });
  }

  // Cookie チェック
  const cookies = parseCookies(request.headers.get('Cookie') || '');
  if (cookies[COOKIE_NAME] === PASSWORD) {
    return next();
  }

  // 未認証 → パスワード画面を表示
  return new Response(renderPage(false, url.pathname), {
    status: 401,
    headers: { 'Content-Type': 'text/html; charset=UTF-8' }
  });
}

function parseCookies(header) {
  const result = {};
  for (const part of header.split(';')) {
    const [k, ...v] = part.trim().split('=');
    if (k) result[k.trim()] = decodeURIComponent(v.join('='));
  }
  return result;
}

function renderPage(error, redirect) {
  return `<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>アクセス</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Hiragino Sans', sans-serif;
      background: #fafafa; min-height: 100vh;
      display: flex; align-items: center; justify-content: center; padding: 20px;
    }
    .card {
      background: #fff; border-radius: 16px;
      box-shadow: 0 2px 24px rgba(0,0,0,0.07);
      padding: 48px 56px; width: 100%; max-width: 400px; text-align: center;
    }
    h1 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
    p { font-size: 14px; color: #6b7280; margin-bottom: 28px; }
    input[type="password"] {
      width: 100%; padding: 12px 16px; font-size: 16px;
      border: 1.5px solid ${error ? '#f87171' : '#e5e7eb'};
      border-radius: 10px; outline: none; margin-bottom: 12px;
      text-align: center; letter-spacing: 0.1em;
    }
    .error { font-size: 13px; color: #ef4444; margin-bottom: 12px; }
    button {
      width: 100%; padding: 13px; background: #1a1a1a; color: #fff;
      border: none; border-radius: 10px; font-size: 15px;
      font-weight: 600; cursor: pointer;
    }
    button:hover { background: #333; }
  </style>
</head>
<body>
  <div class="card">
    <h1>🔒 アクセス制限</h1>
    <p>アクセスするにはパスワードが必要です</p>
    <form method="POST" action="/__auth">
      <input type="hidden" name="redirect" value="${redirect}">
      <input type="password" name="password" placeholder="パスワードを入力" autofocus autocomplete="off">
      ${error ? '<p class="error">パスワードが違います</p>' : ''}
      <button type="submit">入力</button>
    </form>
  </div>
</body>
</html>`;
}

1-3. _headers を作成(ZIPを自動ダウンロードさせる)

/*.zip
  Content-Disposition: attachment

1-3-b. を作成(デプロイ除外設定)

作業中に自動生成されるキャッシュやログをデプロイから除外する:

.claude
.taisun
.wrangler
.workflow_state.json
*.log

1-4. Cloudflare Pages にデプロイ

cd cloudflare/pages
npx wrangler pages deploy . --project-name [プロジェクト名]

初回は対話形式で新規プロジェクト作成を選択する

1-5. パスワードを環境変数に設定

  1. Cloudflare Dashboard → Workers & Pages → [プロジェクト名]
  2. 「設定」→「変数とシークレット」→「追加」
  3. 変数名: SITE_PASSWORD(末尾スペースに注意)
  4. 値: 任意のパスワード(例: pass1234
  5. 「保存して展開」

1-6. パスワード変更方法

Dashboard の SITE_PASSWORD を編集するだけ。再デプロイ不要。


Part 2: アクティベーションキー設定(Cloudflare Worker + KV)

2-1. フォルダ構成

your-project/
└── cloudflare/
    ├── wrangler.toml   ← Worker 設定
    └── worker.js       ← スキル指示を返す認証API

2-2. KV Namespace を作成

cd cloudflare
npx wrangler kv namespace create "activation-keys"

出力された id をメモしておく。

2-3. wrangler.toml を作成

name = "[worker名]"
main = "worker.js"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "KV"
id = "[2-2でメモしたKV ID]"

2-4. worker.js を作成

export default {
  async fetch(request, env) {
    // CORS
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET',
        }
      });
    }

    const url = new URL(request.url);
    const key = url.searchParams.get('key');
    const skill = url.searchParams.get('skill');

    if (!key || !skill) {
      return json({ valid: false, reason: 'missing_params' });
    }

    // KV からキー情報を取得
    const raw = await env.KV.get(key);
    if (!raw) {
      return json({ valid: false, reason: 'invalid_key' });
    }

    const data = JSON.parse(raw);

    // 有効期限チェック
    if (data.expiry && new Date() > new Date(data.expiry)) {
      return json({ valid: false, reason: 'expired', expiry: data.expiry });
    }

    // スキル使用権チェック
    if (!data.skills.includes(skill)) {
      return json({ valid: false, reason: 'skill_not_included' });
    }

    // スキル指示を返す
    const instructions = getInstructions(skill);
    if (!instructions) {
      return json({ valid: false, reason: 'skill_not_found' });
    }

    return json({ valid: true, instructions });
  }
};

function json(data) {
  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    }
  });
}

function getInstructions(skill) {
  // スキルごとの指示を追加する
  const skills = {
    'skill-name': `ここにスキルの指示を記述する`,
    // 'skill2': `...`
  };
  return skills[skill] || null;
}

2-5. Worker をデプロイ

cd cloudflare
npx wrangler deploy

デプロイ後に Worker URL が表示される(例: https://[worker名].workers.dev

2-6. アクティベーションキーを発行

cd cloudflare

npx wrangler kv key put "NK-USER001-20260401" \
  '{"expiry":"2027-01-01","skills":["skill-name"]}' \
  --binding KV --remote
フィールド 説明
キー名 任意の文字列(例: NK-USER001-20260401
expiry 有効期限(ISO 8601形式)
skills 使用できるスキル名の配列

Part 3: 配布ファイル(ZIP)の構成

your-skill/                   ← ZIPに圧縮するフォルダ
├── activate.key              ← ユーザーがキーを入力するファイル
├── .env.example              ← APIキー等の設定テンプレート
├── .claude/
│   └── commands/
│       └── skill-name.md     ← スラッシュコマンド(認証シェル)
└── skill/
    └── skill-name/
        └── (スキルの実行ファイル群)

activate.key の初期内容

PASTE-YOUR-KEY-HERE

.claude/commands/skill-name.md(認証シェル)

# スキル名 — 認証版

## Step 1: アクティベーションキーを確認

プロジェクトルートの `activate.key` を Read ツールで読む。
内容が `PASTE-YOUR-KEY-HERE` のままなら停止してエラーを表示。

## Step 2: 認証APIからスキル指示を取得

\`\`\`bash
node -e "
const path = require('path');
const key = require('fs').readFileSync(path.join(process.cwd(), 'activate.key'), 'utf-8').trim();
const res = await fetch('https://[worker名].workers.dev/?key=' + encodeURIComponent(key) + '&skill=skill-name');
const data = await res.json();
if (!data.valid) {
  const msg = data.reason === 'expired' ? '有効期限切れ(期限: ' + data.expiry + ')' : '無効なキー';
  console.error('❌ エラー: ' + msg);
  process.exit(1);
}
process.stdout.write(data.instructions);
" --input-type=module
\`\`\`

## Step 3: 返ってきた指示に完全に従って実行する

Part 4: 運用コマンド早見表

キー操作

# 発行
npx wrangler kv key put "NK-XXXX" '{"expiry":"2027-01-01","skills":["skill-name"]}' --binding KV --remote

# 無効化(即座に全員停止)
npx wrangler kv key delete "NK-XXXX" --binding KV --remote

# 一覧
npx wrangler kv key list --binding KV --remote

Pages 再デプロイ(パスワード変更後など)

cd cloudflare/pages
npx wrangler pages deploy . --project-name [プロジェクト名] --commit-dirty=true

Worker 再デプロイ(スキル内容を更新したとき)

cd cloudflare
npx wrangler deploy

Part 5: トラブルシューティング

症状 原因 対処
パスワードが通らない 環境変数名に末尾スペースがある Dashboard で変数名を削除して再作成
キーが無効と出る --remote なしで発行した --remote フラグを付けて再発行
スキルが見つからない wrangler が別アカウントでログイン中 wrangler logoutwrangler login
/コマンド名 が出ない Claude Codeが親フォルダを開いている activate.key があるフォルダを直接開く

セキュリティ上の注意


Part 6: デプロイ前セキュリティチェックリスト

新しいスキルにこの2段階構成を適用するたびに、デプロイ前に必ず確認する。


6-1. 機密情報の分類と管理

情報の種類 置き場所 Git管理 備考
スキルの指示・ノウハウ(コアプロンプト) worker.js 内 または Cloudflare Secret ❌ 除外 キーなしで取得不可
note.com ログイン情報 .env(ローカルのみ) ❌ 除外 .env.example だけコミット可
Gemini / 外部APIキー .env(ローカルのみ) ❌ 除外 Cloudflare側はSecrets変数を使う
サイトパスワード(SITE_PASSWORD Cloudflare Dashboard の環境変数 ❌ 記載しない wrangler.toml に直書き禁止
KV Namespace ID wrangler.toml ✅ OK IDだけでは読み取り不可
activate.key(発行済みキー) ユーザーのローカルのみ ❌ 除外 配布ZIPには PASTE-YOUR-KEY-HERE のプレースホルダーを入れる

Cloudflare Worker にシークレットを渡す場合(APIキーなど):

# wrangler.toml に直書きせず、Secrets として登録する
npx wrangler secret put SECRET_NAME
# → プロンプトで値を入力(履歴に残らない)

6-2. JSファイルのログ除去チェック

デプロイ前に worker.js_middleware.js に不要なログが残っていないか確認する。

grepで一括チェック(プロジェクトルートから実行):

grep -rn "console\.\(log\|debug\|info\|warn\)" cloudflare/

出力があった場合は削除してからデプロイする。

残してよいもの / 削除すべきもの:

種類 対応
console.error(...) ✅ 残してOK(エラー検知に必要)
console.log("key:", key) ❌ 削除必須(機密情報が漏れる)
console.debug(...) ❌ 削除(本番不要)
console.info(...) ❌ 削除(本番不要)
デバッグ用の return json({debug: ...}) ❌ 削除必須

6-3. 配布ZIPに含めてはいけないファイル

ZIP圧縮前に以下が含まれていないことを確認する:

# 危険なファイルが混入していないか確認
grep -r "NK-" dist/          # 実際の発行済みキーが残っていないか
grep -r "GEMINI_API_KEY=" dist/ --include="*.env" 2>/dev/null  # 実APIキーが残っていないか
含めてはいけないファイル 理由
.env(実際の値入り) APIキー・パスワードが漏洩する
activate.key(実際のキー入り) 不正利用される
output/(生成済み記事・画像) 個人情報・著作物が混入する可能性
.claude/(会話履歴・フック) 内部構造が露出する
skill/*/SKILL.md(フル指示) ノウハウ・コアプロンプトが漏洩する
node_modules/ 容量肥大・不要

ZIP圧縮前の最終確認コマンド:

# dist/your-skill/ の中身を一覧表示して目視確認
ls -la dist/your-skill/
ls -la dist/your-skill/.claude/ 2>/dev/null && echo "⚠️ .claude/ が含まれています"

6-4. デプロイ前チェックリスト(コピーして使う)

【Pages デプロイ前】
□ _middleware.js に console.log が残っていない
□ SITE_PASSWORD を wrangler.toml / index.html に直書きしていない
□ .claudeignore または cloudflare/pages/.cfignore で不要ファイルを除外している

【Worker デプロイ前】
□ worker.js に console.log / console.debug が残っていない
□ APIキー・トークンを worker.js にハードコードしていない(Secrets変数を使用)
□ デバッグ用のレスポンス(debug フィールド等)を削除している

【ZIP配布前】
□ activate.key の中身が PASTE-YOUR-KEY-HERE になっている
□ .env.example のみ含まれ、.env(実値)は含まれていない
□ output/ .claude/ node_modules/ が含まれていない
□ grep -rn "console\.log" cloudflare/ で出力がゼロである

↑ トップへ戻る