#security (5)
必須対応
1. permissions の明示設定
未設定だとデフォルト権限がリポジトリ設定依存で過剰になりうる。最小権限を明示する。
permissions:
contents: read
2. ${{ }} インジェクション対策
run: ブロック内で outputs を直接展開するとシェルインジェクションのリスク。環境変数経由で渡す。
# NG: 直接展開
run: echo "$\{\{ steps.changed.outputs.files \}\}"
# OK: 環境変数経由
env:
CHANGED_FILES: $\{\{ steps.changed.outputs.files \}\}
run: echo "$CHANGED_FILES"
3. SHAピンニング
タグ(@v4)は上書き可能。フルコミットSHAで固定する。2025年3月のtj-actions/changed-files事件(CVE-2025-30066)が実例。
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
4. EOFデリミタのランダム化
マルチライン出力で固定文字列EOFを使うと内容に含まれた場合に破壊される。
DELIMITER=$(openssl rand -hex 16)
echo "output<<$DELIMITER" >> "$GITHUB_OUTPUT"
echo "$CONTENT" >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
推奨対応
5. workflow_dispatch の入力パラメータ
手動実行時はHEAD~1比較が意図通り動かないことがある。全件処理モードを用意する。
概要
reCAPTCHA v3 から Cloudflare Turnstile へ移行する際の実装パターン。Turnstile は無料・プライバシーフレンドリーで CF Pages ネイティブ対応。
キー情報
- テストキー
- サイトキー:
1x00000000000000000000AA - シークレット:
1x0000000000000000000AA
- サイトキー:
- 本番環境: Cloudflare Dashboard で生成
クライアント側(React/Svelte)
// CDN 読み込み(head に挿入)
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
// Explicit rendering(推奨)
turnstile.render('#turnstile-container', {
siteKey: import.meta.env.PUBLIC_TURNSTILE_KEY,
theme: 'light',
callback: (token) => {
// トークン取得後の処理(フォーム送信など)
console.log('Turnstile token:', token);
}
});
// リセット(フォーム送信後)
turnstile.reset();
サーバー側(CF Pages Functions / Node.js)
// POST /api/verify-captcha
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: env.TURNSTILE_SECRET_KEY,
response: token
})
});
const data = await response.json();
if (!data.success) {
throw new Error('Captcha verification failed');
}
メリット
- reCAPTCHA より実装が簡単
- ユーザー側のプライバシー尊重(データ共有なし)
- CF Pages との相性が良い
- 無料で使用可能
概要
Firebase Cloud Functions のシークレット(API キー、Webhook URL など)は Google Cloud Secret Manager で一元管理する。
登録方法
# シークレットを登録
firebase functions:secrets:set SECRET_NAME
# ユーザー入力を促される(パスト可能)
# または CI/CD で:
firebase functions:secrets:set SECRET_NAME --data "secret-value"
関数での使用
// functions/src/index.ts
import * as functions from 'firebase-functions/v2';
export const sendMail = functions.https.onCall(
{
secrets: ['SLACK_WEBHOOK_URL', 'SENDGRID_API_KEY']
},
async (request, context) => {
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
const apiKey = process.env.SENDGRID_API_KEY;
// 処理...
}
);
取得方法
デプロイ後、シークレット値を確認する場合:
firebase functions:secrets:access SECRET_NAME
ベストプラクティス
- secrets 配列に列挙する → 必要な値のみロード、セキュリティ向上
- Git リポジトリにはコミットしない → Secret Manager で一元管理
- 環境別に別々のシークレット → dev / prod で切り分け
注意
Astro + CF Functions へ移行する場合は wrangler secrets に移行する。
背景
- Google reCAPTCHA は Google infrastructure 依存
- Cloudflare Pages への移行に伴い、Cloudflare 内部で完結する Turnstile に変更
差異比較
| 項目 | Google reCAPTCHA | Cloudflare Turnstile |
|---|---|---|
| チャレンジ | ロボット判定 | ロボット判定 |
| パフォーマンス | 速い | より高速(CF エッジで実行) |
| プライバシー | Google に送信 | CF 内部で完結 |
| 料金 | 無料 | 無料(CF Pages Free含む) |
| デプロイ | Google Console | wrangler.toml + env var |
| キャプチャUI | Google ブランド | CF ブランド(かつシンプル) |
実装
1. Cloudflare 側
# wrangler.toml
[[env.production.r2_buckets]]
name = "TURNSTILE_SECRET_KEY"
binding = "TURNSTILE_SECRET_KEY"
[env.production.vars]
TURNSTILE_SITE_KEY = "1x..."
2. フロント(Astro コンポーネント)
---
import { TURNSTILE_SITE_KEY } from 'astro:env/client';
---
<form id="contact-form">
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<!-- Turnstile ウィジェット -->
<div class="cf-turnstile" data-sitekey={TURNSTILE_SITE_KEY}></div>
<button type="submit">送信</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script>
document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const token = window.turnstile.getResponse();
if (!token) {
alert('Please complete the verification');
return;
}
const formData = new FormData(e.target);
formData.append('cf-turnstile-response', token);
await fetch('/api/contact', {
method: 'POST',
body: new URLSearchParams(formData)
});
});
</script>
3. バック(Pages Functions)
// functions/contact.ts
export const onRequest: PagesFunction = async (context) => {
const formData = await context.request.formData();
const token = formData.get('cf-turnstile-response');
// Turnstile トークン検証
const verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const verifyResponse = await fetch(verifyUrl, {
method: 'POST',
body: JSON.stringify({
secret: context.env.TURNSTILE_SECRET_KEY,
response: token
})
});
const data = await verifyResponse.json();
if (!data.success) {
return new Response('Verification failed', { status: 400 });
}
// フォーム処理続行...
};
メリット
- プライバシー: Google と通信なし
- 速度: Cloudflare エッジで即座に検証
- 一体性: Pages + Turnstile で CF 内完結
- UI/UX: Turnstile のキャプチャがシンプル
移行時の注意
- Turnstile は reCAPTCHA v3(スコアベース)ではなく v2 相当(チャレンジベース)
- 既存のスコアロジックがあれば別途実装が必要
Cloudflare Pages アプリのアクセス制限
プライベート用アプリにアクセス制限をかける3つの方法:
| 方法 | コスト | 特徴 |
|---|---|---|
| Cloudflare Access (Zero Trust) | 無料50ユーザー | GCP IAP相当。コード変更不要 |
| Basic認証(Honoミドルウェア) | 無料 | UXが悪い |
| OAuth許可リスト | 無料 | 既存OAuth活用 |
Cloudflare Access が最適な理由
- コード変更ゼロ: ダッシュボード設定のみで前段にログインゲートが入る
- 既存のアプリ内認証(Google OAuthなど)と共存可能
- メール、Google、GitHubなど複数のIdPに対応
- デフォルトでワンタイムPIN(メール認証)が使える
設定の流れ
Zero Trust Dashboard > Access > Applications > Self-hosted
→ ドメイン指定 → Policy で許可メールアドレス設定 → 完了
GCP IAP的なことをやりたい場合、Cloudflareならこれ一択。