コマンド集 / Cloudflare

Cloudflare コマンド集

Cloudflare Tunnel / Access / DNS / Workers ルート / WARP を API と CLI で扱う手順を、トークンの作法、公開の順序、閉め出されたときの退路、ホームラボで実際に踏んだ罠つきで引ける。

更新

ラボの外からの入口は Cloudflare Tunnel 1 本だけで、コネクタ(cloudflared)は dns01 / dns02 の 2 台に置き、公開名はすべて Cloudflare Access(Entra ID)の裏にある環境向け。API は https://api.cloudflare.com/client/v4Authorization: Bearer $CF で呼ぶ。人の端末は WARP でプライベートネットワーク(<ラボのCIDR>)へ届く。

トークンと API の作法#

トークンを確かめる#

最初に打つ。statusactive でなければ先に進まない。

CF="$(op read op://<vault>/<item>/credential)"
curl -s https://api.cloudflare.com/client/v4/user/tokens/verify -H "Authorization: Bearer $CF" | jq '.result.status'

⚠️ Bearer で弾かれて X-Auth-Email + X-Auth-Key なら通る、という値は API トークンではなく Global API Key。ゾーン削除も課金操作もできる親鍵なので、それを 1 回だけ使って権限を絞ったトークンを作り、親鍵はラボから読める場所に置かない。

最小権限の切り方#

アカウント権限は Tunnel / Access: Apps and Policies の Edit、DNS の Edit は対象ゾーンだけに絞る。Workers は別トークンにする。

Account  Cloudflare Tunnel: Edit
Account  Access: Apps and Policies: Edit
Zone     DNS: Edit  (Include: Specific zone)

⚠️ GET /accounts が空を返しても「権限が無い」とは限らない(Account Settings 権限を付けていないだけ)。アカウント ID はゾーンから取る。この誤判断で 2 回、不要な手作業を人に頼んだ実例あり。

アカウント ID とゾーン ID を取る#

curl -s "https://api.cloudflare.com/client/v4/zones?name=example.jp" -H "Authorization: Bearer $CF" \
  | jq -r '.result[0] | "\(.id) \(.account.id)"'

⚠️ cloudflaredcert.pem に入っているゾーン ID は削除済みの旧ゾーンのことがある。そのまま使うと別ゾーンを操作する。

設定系 API は全置換(PUT)#

トンネル設定・Split Tunnel・Local Domain Fallback・Access アプリはどれも PUT で丸ごと置き換わる。読む → 差分を当てる → 書き戻す → 再取得して失われたものが無いか見る。 ⚠️ PATCH10405 Method not allowed for this authentication scheme で落ちるが、PUT は通る。PATCH が落ちたのを見て「更新系は全部だめ」と一般化した実例あり。

Tunnel(cloudflared)#

入れる・トークンを置く#

トークンはファイルから読ませる。--token でコマンドラインに置くと ps で見える。

curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
dpkg -i cloudflared.deb
install -d -m 700 /etc/cloudflared
op read "op://<vault>/<item>/credential" | tr -d '\r\n' > /etc/cloudflared/tunnel-token
chmod 600 /etc/cloudflared/tunnel-token

systemd ユニット#

cp cloudflared.service /etc/systemd/system/ && systemctl daemon-reload && systemctl enable --now cloudflared

⚠️ 外向きは UDP/7844(QUIC)。TCP/443 しか開けていない egress ルールを当てると、公開経路が全部止まる。tcp/7844 は http2 に落ちたとき用。

ingress を API で足す#

catch-all(hostname の無い要素)は必ず末尾。現行を退避してから、その手前に挿す。

TUN=<トンネルID>
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TUN/configurations" \
  -H "Authorization: Bearer $CF" | jq '.result.config' > ingress-before.json
jq '.ingress = ((.ingress|map(select(.hostname != null)))
  + [{"hostname":"<公開ホスト名>","service":"https://<内部FQDN>:8006"}]
  + (.ingress|map(select(.hostname == null))))' ingress-before.json > ingress-after.json
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TUN/configurations" \
  -H "Authorization: Bearer $CF" -H "Content-Type: application/json" -d "{\"config\": $(cat ingress-after.json)}"

⚠️ 既存のホスト名を含めて送らないと消える。 送る前に ingress-after.json に既存が残っているか目で確かめる。

オリジンは名前で指す(noTLSVerify を使わない)#

FQDN ならエッジからオリジンまで検証済み TLS で通る。IP を指すと証明書名が一致せず noTLSVerify が要り、取った証明書が無意味になる。

https://<内部FQDN>:8006                  TLS 検証あり
https://localhost:53443  + no_tls_verify: true      自己署名。loopback を出ないので可

⚠️ PVE の OIDC はログイン途中の state がノードローカル。2 台に解決する名前をオリジンにすると、cloudflared が往復を散らして必ず失敗する。noVNC も「チケットを発行したノード」に WebSocket が要るので同じく壊れる。PVE のオリジンは 1 台に固定する(3 ノードにしても直らない)。

コネクタに何が降りたか#

ダッシュボードの表示ではなく、コネクタに実際に届いた設定が真実。version=N が増えていなければ保存できていない。

journalctl -u cloudflared --no-pager -o cat | grep 'Updated to new configuration' | tail -1
journalctl -u cloudflared --no-pager -n 50 | grep -E 'Registered tunnel connection|protocol='

⚠️ ダッシュボードで「No TLS Verify」を切り替えると URL 欄まで隣のルートの値に書き換わることがある。編集後は必ずこのログで確かめる。

Access(アプリとポリシー)#

作る順序と消す順序#

Access アプリ → Workers ルート → DNS レコードの順。逆にするとその間、誰でも開ける。消すときは逆順(ingress → DNS → Access アプリ)。

curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$ACC/access/apps" \
  -H "Authorization: Bearer $CF" -H "Content-Type: application/json" -d '{
  "name": "<アプリ名>", "type": "self_hosted", "domain": "<公開ホスト名>",
  "session_duration": "6h", "app_launcher_visible": true, "http_only_cookie_attribute": true,
  "policies": [{"name": "Allow me", "decision": "allow", "precedence": 1,
                "include": [{"login_method": {"id": "<IdPのID>"}}]}]}'

⚠️ Access アプリだけ先に消して Grafana が無認証で公開された実例あり(auth proxy がコネクタの IP を信頼していた)。トンネルの裏のサービスは、コネクタの IP を認証根拠にしない。

「Allow me」は「私」ではない#

includelogin_method だけのポリシーは「その IdP で認証できる者は全員」。テナントに同期で増えた利用者も通る。締めるのは IdP 側(Entra のエンタープライズアプリを割り当て必須に)が 1 箇所で済む。

az rest --method PATCH --url "https://graph.microsoft.com/v1.0/servicePrincipals/<SP_ID>" \
  --headers "Content-Type=application/json" --body '{"appRoleAssignmentRequired": true}'

⚠️ 先に自分を割り当て、通ることを確認してから倒す。 順序を間違えると全 Access アプリから締め出される。ポリシー名を信じず include / require / exclude の中身を読む。include は OR で、行を足すほど緩くなる。AND にしたいものは require

サービストークン#

ヘッダは CF-Access-Client-Id / CF-Access-Client-Secret

curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/access/service_tokens" -H "Authorization: Bearer $CF" \
  | jq '.result[] | {name, client_id, expires_at}'

⚠️ ダッシュボードの「ヘッダー形式でコピー」をそのまま保管すると、値に CF-Access-Client-Id: の接頭辞が付く(長さが 39/54 でなく 60/79)。登録が Does not exist in API で黙って落ちる。削除はポリシー → トークンの順(12139 service_token_in_use)。アプリに紐づかない再利用ポリシーも参照元になる。everyone + decision=bypass のポリシーは付け間違えた瞬間にそのアプリが無防備になるので置かない。

OIDC の二層目(PVE / Technitium)#

Access を通った先で PVE 自身が Entra にもう一段ログインする。PVE はアドレスバーの文字列をそのまま redirect_uri に送るので、公開名を変える前に Entra の返り先を直す。

az ad app update --id <appId> --web-redirect-uris https://<公開ホスト> https://<内部FQDN>:8006 https://<内部FQDN>:8006

⚠️ --web-redirect-uris全置換。既存を並べ直して渡す。IP で開くと AADSTS50011 になるが、IP を登録して解決しない(証明書が張れない)。名前で開くのが修正。改名で 2 回続けて踏んだ実例あり。

入口が生きているか#

302 でログイン画面に飛べば DNS・エッジ・コネクタ・オリジンが全部生きている。API パスまで見る。

for p in / /api/status /healthz; do curl -s -o /dev/null -w "%{http_code} $p\n" "https://<公開ホスト名>$p"; done

⚠️ 302 は「入口が生きている」証拠であって「ログインできる」証拠ではない。 OIDC の往復までは試していない。App Launcher は https://<チーム名>.cloudflareaccess.com

DNS レコード#

トンネル向けの CNAME を足す#

トンネル越しは CNAME <トンネルID>.cfargotunnel.com、エッジで処理するもの(Worker / ブラウザ RDP / apex)は AAAA 100::。どちらも proxied。

curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records" \
  -H "Authorization: Bearer $CF" -H "Content-Type: application/json" \
  -d '{"type":"CNAME","name":"<ホスト部>","content":"<トンネルID>.cfargotunnel.com","proxied":true,"comment":"<用途>"}'

⚠️ proxied=false にすると Access も Workers ルートも効かない。ダミーの A 240.0.0.01002 で拒否される。ブラウザ RDP の名前をトンネルの CNAME にすると 404 / 522 になる。

作る前に引かない・作った後は権威に聞く#

作成前に内側の DNS で引くと NXDOMAIN が最大 30 分キャッシュされ、ラボの中だけ引けない状態になる。同じ失敗を 2 回やったので、作成後にその名前だけキャッシュを消す手順を道具に入れた。

dig @<権威サーバ> <公開ホスト> A +short
curl -s -o /dev/null -w '%{http_code}\n' --resolve <公開ホスト>:443:<エッジIP> https://<公開ホスト>/
curl -s "http://<内部DNSのIP>:5380/api/cache/delete?token=$T&domain=<公開ホスト名>"

⚠️ proxied なレコードは dig CNAME で引いても回答が返らない(エッジで A/AAAA に平坦化される)。type=A で引く。内側ゾーン名(lab.example.jp)を公開側で使わない。WARP 端末が内と外のどちらを引くか不定になる。

Workers ルート#

配備とルート#

ルートはゾーン単位で、ホスト名とパスまで絞って入れる。

cd ops/home-page && npx wrangler deploy
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/workers/routes" \
  -H "Authorization: Bearer $CF_WORKERS" -H "Content-Type: application/json" \
  -d '{"pattern":"<公開ホスト名>/*","script":"<Worker名>"}'
npx wrangler versions deploy <前の>@100%

⚠️ *.example.jp/* を絶対に入れない。 「1 つだけ」ではなく「全部」の意味で、2026-09-15 に実際に全サービスを横取りした。workers_dev = false なら wrangler deployNo targets deployed と言うのは正しい。Workers のトークンはトンネル用と別。

WARP(Zero Trust クライアントとプライベートネットワーク)#

端末を登録する#

GUI はブラウザが開かなくても理由を出さない。CLI なら理由と開くべき URL が出る。

winget install -e --id Cloudflare.Warp
cd "C:\Program Files\Cloudflare\Cloudflare WARP"
.\warp-cli.exe registration new <チーム名>
.\warp-cli.exe status
.\warp-cli.exe --accept-tos settings | Select-String 'Profile ID|Always On|Mode'

⚠️ ブラウザが開かないときは、その PC で https://<チーム名>.cloudflareaccess.com/warp を直接開く。IdP のログインが出ればネットワークは正常。Windows Server には入らない1603。製品の非対応で設定では回避できない)。ログは C:\ProgramData\Cloudflare\cfwarp_service_log.txt

どのプロファイルに当たったか#

API では取れない。端末側の Profile IDdefault なら一致条件が外れている。

warp-cli --accept-tos settings | Select-String 'Profile ID'

⚠️ サービストークンで登録した端末の identity は non_identity@<チーム名>.cloudflareaccess.com になる。一致条件はメールではなく identity.service_token_uuidAPI は綴りを検査せず、静かに既定へ落ちる。 mdm.xmlservice_mode を書くとプロファイルより強くなる(organization だけ書く)。API の値は warp / 1dot1 / proxy / posture_only / warp_tunnel_only の 5 つだけ。

Split Tunnel と Local Domain Fallback#

既定の除外 10.0.0.0/8 はラボの <ラボのCIDR> を飲み込む。丸ごと消すと出先の 10.x な LAN に届かなくなるので、ラボ分だけ引いた CIDR に分解して置き換える。自ゾーンは Fallback で内側の DNS へ向ける。

curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/devices/policy/exclude" -H "Authorization: Bearer $CF" | jq '.result[].address'
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACC/devices/policy/fallback_domains" -H "Authorization: Bearer $CF" | jq '.result[] | {suffix, dns_server}'

⚠️ どちらも PUT で全置換。新規プロファイルは Split Tunnel / Fallback が既定値で作られるので、作ったら経路の設定を別途コピーする。suffix は暗黙にサブドメインを含む。

Gateway が最後に落とす#

Private Network / Split Tunnel / Fallback が全部正しくても、Gateway の L4 が 10.0.0.0/8 を block していると「Connected なのに何も届かない」になる。deny は残し、ラボ宛の allow を上に置く。

precedence 6000  ALLOW  net.dst.ip in {<ラボのCIDR>}
precedence 10000 BLOCK  Default deny for private traffic

⚠️ allow に identity 条件を付けない。Windows のサインイン前接続はユーザー識別が無く、AD への到達が壊れる。

CGNAT 帯と内部名の確認#

WARP 端末は 100.64.0.0/10 から来る。ファイアウォールのログでこれを「インターネット宛」と数えると報告が嘘になる。

ip route get 100.96.0.1
curl -k -s -o /dev/null -w '%{http_code}\n' https://<IP>:8006/
curl -k -s -o /dev/null -w '%{http_code}\n' https://<内部FQDN>:8006/

⚠️ 切り分けは上から順に。Connected か → IP で届くか(NG なら Gateway か Split Tunnel)→ 名前で届くか(NG なら Fallback)。

マネージドネットワーク(「家にいる」判定)#

判定は内部の TLS エンドポイントの証明書指紋。Let’s Encrypt の更新で必ず変わるので、実物を読んで登録する。

echo | openssl s_client -connect <IP>:8006 2>/dev/null | openssl x509 -noout -fingerprint -sha256

⚠️ 検出用エンドポイントは全プロファイルから自動除外される(その機械には外から WARP で届かなくなる)。トンネルで別に公開している機械を選ぶ。指紋が外れても通信は止まらず遅くなるだけなので、気づく手がかりが無い。

break-glass#

Cloudflare を通らない退路#

Access が壊れても回線が死んでも入れる経路を、公開経路とは別に必ず 1 つ残す。内部名で直接開き、IdP ではなくローカルの認証で入る形にしておく。

内部の FQDN + ローカル認証(IdP を経由しない)

⚠️ 「公開名しか知らない」状態にしない。管理画面を触りたいのはネットワークが不調なときで、そのとき公開名は死んでいる。既定レルムを IdP にしても、ログイン画面でローカル認証に切り替えられることを平時に確かめておく。

閉め出されたら見る順#

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://<公開ホスト>/
journalctl -u cloudflared --no-pager -n 30
grep openid /var/log/pveproxy/access.log | tail -5

⚠️ 送信元がコネクタの IP なら公開経路、クライアントの IP なら直アクセス。openid/auth-url の後に openid/login が無ければ IdP 側で弾かれている。Entra の「割り当て必須」を倒した直後は自分の割り当てを疑う。

鍵に依存しない到達(Azure Arc run-command)#

sshd が死んでいても Entra の RBAC だけで入れる。使い捨てにして、結果を読んだら必ず delete する。

az connectedmachine run-command create -g <RG> --machine-name <マシン> --run-command-name bg-test --location <リージョ> --script "hostname"
az connectedmachine run-command delete -g <RG> --machine-name <マシン> --run-command-name bg-test

⚠️ 消さないとホストに /var/lib/waagent/runcommandhandlerlinux.<名前>.prv の秘密鍵が積み上がる。同一マシンへ deletecreate を並行させると作成側が消える(HCRP404)。ARM の状態は 20 分以上遅れることがあるので、結果は別経路(TLS を張る・サービスを叩く)で見る。

困ったとき#

502#

オリジン側。IP を指して証明書名が不一致か、オリジンのノードが落ちている。コネクタの中から curl -s -o /dev/null -w '%{http_code} ssl_verify=%{ssl_verify_result}\n' https://<内部FQDN>:8006/ で直接叩き、200 ssl_verify=0 を確かめる。

WARP は Connected なのに何も届かない#

層が多く、症状が全部同じ。Private Network ルート → Split Tunnel → Local Domain Fallback → 登録ポリシー → デバイスプロファイル(service_mode)→ Gateway L4 の順に全部読む。1dot1(DNS のみ)のプロファイルではプライベートネットワークは通らない。