「Dev では塞いだのに、Prod だけ穴が開いている」を Sysdig の Zone で検知する
この記事の主張
Dev と Prod は「最初は同じ」でも、必ずズレる。そのズレ(環境ドリフト)こそが本番事故の入り口であり、IaC の外で起きた変更まで含めて拾うには、クラウドの実態を持っている CNAPP 側で環境を比較するのが一番速い。
本記事では Sysdig Secure の Zone を「環境という論理境界」として使い、Dev/Prod の
設定差分(Security Group / WAF など) と ネットワーク露出差分 を取る方法を、
なぜそれが重要なのかとセットで解説します。Terraform と汎用スクリプトも同梱します。
1. なぜ「環境ドリフト」が怖いのか
Dev と Prod は、デプロイ直後は同じ構成のはずです。しかし時間が経つと——
- 障害対応で Prod の Security Group に手動で 22/tcp を開けたまま戻し忘れる
- Dev では WAF に Core Rule Set を足したが、Prod の WAF には反映されていない
- Terraform を介さないコンソール操作で、Prod の S3 だけ versioning が切れている
こうした「本番だけ弱い」状態は、レビューもアラートも素通りしがちです。
ドリフトは「悪意ある攻撃」ではなく「善意の運用」から生まれるので、検知が後手に回ります。
だからこそ、環境間の差分を機械的・継続的に可視化できる仕組みが要ります。
2. なぜ「CNAPP 側で見る」と嬉しいのか
「IaC の diff を見ればいいのでは?」——半分正解です。が、IaC diff には穴があります。
| 観点 | IaC の diff | CNAPP(実態)で環境比較 |
|---|---|---|
| 手動変更・コンソール操作 | 追えない(state とズレる) | ✅ 実態を見るので拾える |
| IaC 管理外のリソース | そもそも見えない | ✅ インベントリに居れば見える |
| 複数アカウント/リージョン横断 | repo がバラバラだと困難 | ✅ 1テナントに集約して比較 |
| 「本番だけ弱い」の継続監視 | PR 時点のみ | ✅ 定期実行で常時 |
| 監査エビデンス | コードの意図 | ✅ 実際の状態のスナップショット |
つまり CNAPP 側の環境比較は、「意図(IaC)」ではなく「現実(実態)」を、環境横断で継続的に突き合わせる点に価値があります。Sysdig ではその「環境」を Zone で表現します。
3. Zone とは何か(論理モデル)
手を動かす前に、Zone を論理的に定義します。ここを誤ると設計を間違えます。
Sysdig は観測対象(クラウドリソース、K8s ワークロード、ホスト、イメージ…)を
エンティティの集合=インベントリ・グラフとして持っています。
Zone とは、このグラフを「属性条件で切り出した部分集合」に名前を付けたもの。
リソースを物理的に移動・所有するのではなく、「この条件に合うものを prod と呼ぶ」という宣言的な射影です。
Zone のメンバーは評価時点で条件照合して動的に決まります(リソースを足せば自動で入る)。
静的な所属リストではなく、**集合の内包的定義(条件)**です。だから「環境」のような動く境界に向きます。
3つの「スコープ」は直交している(最重要)
Sysdig には「範囲を絞る」概念が 3つ あり、互いに独立した軸です。Zone 理解の核心はこの区別です。
| 軸 | 何を絞るか | 例 |
|---|---|---|
| Zone | 結果の見え方 | 「prod の findings だけ表示」 |
| Policy scope | ポリシーの適用範囲 | 「この Runtime ルールは prod だけ」 |
| Teams + RBAC | アクセス制御 | 「Dev チームは dev だけ閲覧可」 |
⚠️ Zone はポリシースコープでもアクセス制御でもない。 「prod Zone を作ればポリシーが prod だけに効く / 権限が分かれる」は誤りです。範囲は Policy 側、権限は RBAC 側。
判定法はシンプル:結果のビューを絞る話なら Zone、適用範囲なら Policy、権限なら RBAC。
4. 全体像:環境ドリフトを3層に振り分ける
「環境ドリフト」と一口に言っても、見たいものによって使う機構が違います。混ぜると詰みます。
本記事は ①。データフローはこうです。
第3節の定義に従えば、これは
「環境という論理境界をグラフへ射影(Zone)→ 各要素の状態を読む(Config API)→ 2つを比較(diff)」
という一直線の帰結です。
5. Step 1:Zone を作る(Terraform)
terraform {
required_providers {
sysdig = { source = "sysdiglabs/sysdig", version = "~> 1.0" }
}
}
# 認証は環境変数から(トークンをコードに書かない)
# export SYSDIG_SECURE_API_TOKEN=xxxxxxxx
# export SYSDIG_SECURE_URL=https://<your-region>.app.sysdig.com
provider "sysdig" {}
locals { aws_account = "123456789012" } # ← あなたの AWS アカウント ID
resource "sysdig_secure_zone" "dev" {
name = "dev"
scope {
target_type = "aws"
rules = "account in (\"${local.aws_account}\") and location in (\"us-east-1\")"
}
}
resource "sysdig_secure_zone" "prod" {
name = "prod"
scope {
target_type = "aws"
rules = "account in (\"${local.aws_account}\") and location in (\"ap-northeast-1\")"
}
}
terraform init && terraform apply
💡 検証コラム①:AWS の Zone は「リソースタグ」では切れない
Environment=dev/prodのタグで切りたくてrules = "labels in (\"Project: MyApp\")"を試したら、
Zone メンバー 0 件。ところが同じ式を Inventory API に投げると 18 件ヒットします。
理由:AWS の Zone スコープのlabelsは アカウントラベル空間で、EC2/SG のリソースタグとは別空間(同綴り別物)。
→ タグで環境を分けたいなら 後段の Inventory フィルタ側でlabels in (...)する、または 環境を別アカウントに分けて account スコープにする。K8s ならlabel.<key>が効く。
💡 検証コラム②:
expressionブロックは安定版 Provider 未対応
公式 master のexpression { field/operator/values }は安定版(v1.60.0)でUnsupported block type。
安定版では legacy のrules文字列を使う(account/locationは v2 互換で警告も出ない)。
6. Step 2:Zone で対象を絞る
-- SysQL:Zone 別の件数(スコープが効いているか確認)
MATCH CloudResource IN Zone WHERE Zone.name =~ 'dev|prod'
RETURN Zone.name, count(DISTINCT CloudResource) AS cnt ORDER BY Zone.name
# Inventory API:zone in ("dev") でスコープ
curl -s -H "Authorization: Bearer $SYSDIG_SECURE_API_TOKEN" \
"$SYSDIG_SECURE_URL/api/cspm/v1/inventory/resources?filter=$(python3 -c 'import urllib.parse;print(urllib.parse.quote("zone in (\"dev\") and type=\"VPC Security Group\""))')&pageSize=300"
各リソースに hash / configApiEndpoint / labels / isExposed / zones が付きます。
💡 検証コラム③:SysQL は「生ルール」を持っていない
MATCH CloudResource RETURN CloudResourceで返るのは name/type/account/region/isExposed などのメタデータまで。
SG の ingress(ポート/CIDR)や WAF のルール本体は含まれない。生ルールは次の Config API が必要。
7. Step 3:設定の中身(生 config)を Config API で取る
curl -s -H "Authorization: Bearer $SYSDIG_SECURE_API_TOKEN" \
"$SYSDIG_SECURE_URL/api/cspm/v1/cloud/resource?resourceHash=<hash>&resourceKind=VPC+Security+Group"
configuration(文字列化 JSON)にクラウドの生 config が丸ごと入ります。
"IpPermissions": [
{ "FromPort":22, "ToPort":22, "IpProtocol":"tcp", "IpRanges":[{"CidrIp":"0.0.0.0/0"}] }
]
WAF(Web V2 ACL)なら Rules に各ルール(RateBased / ManagedRuleGroup / Action)がそのまま入ります。
💡 検証コラム④:Config API は型横断(S3 だけ注意)
SG/WAF に加え IAM Policy/Role・EKS Cluster・EC2・RDS でもconfigurationに中身あり。
ただし S3 は versioning/encryption/public-access が別リソース型に分かれており、バケット本体 config には無い。
8. Step 4:Dev/Prod 差分を出す(汎用スクリプト)
任意の 2 Zone × 任意リソース種別で差分を出す汎用ツール(全文は同梱の zone_config_diff.py)。
#!/usr/bin/env python3
import argparse, json, os, urllib.parse, urllib.request
TOKEN, BASE = os.environ["SYSDIG_SECURE_API_TOKEN"], os.environ["SYSDIG_SECURE_URL"].rstrip("/")
def api(p):
r = urllib.request.Request(BASE+p, headers={"Authorization": f"Bearer {TOKEN}"})
return json.load(urllib.request.urlopen(r, timeout=30))
def inventory(zone, rtype):
q = urllib.parse.quote(f'zone in ("{zone}") and type="{rtype}"')
return api(f"/api/cspm/v1/inventory/resources?filter={q}&pageSize=500")["data"]
def sg_public_ingress(conf):
out = set()
for p in conf.get("IpPermissions", []):
proto, fp, tp = p.get("IpProtocol"), p.get("FromPort"), p.get("ToPort")
port = "all" if proto == "-1" else (str(fp) if fp == tp else f"{fp}-{tp}")
if any(r.get("CidrIp","").endswith("/0") for r in p.get("IpRanges", [])):
out.add(f"{proto}/{port}")
return out
def collect(zone, rtype):
rows = {}
for r in inventory(zone, rtype):
conf = json.loads(api(r["configApiEndpoint"])["data"].get("configuration","{}") or "{}")
name = conf.get("GroupName") or r["name"]
rows[name] = sg_public_ingress(conf) if rtype=="VPC Security Group" else set(conf.keys())
return rows
ap = argparse.ArgumentParser()
ap.add_argument("--zone-a"); ap.add_argument("--zone-b"); ap.add_argument("--type", default="VPC Security Group")
args = ap.parse_args()
a, b = collect(args.zone_a, args.type), collect(args.zone_b, args.type)
pa = set().union(*a.values()) if a else set()
pb = set().union(*b.values()) if b else set()
print(f"{args.zone_a} のみ:", sorted(pa-pb) or "なし")
print(f"{args.zone_b} のみ:", sorted(pb-pa) or "なし")
print("共通:", sorted(pa&pb))
=== VPC Security Group: dev vs prod ===
[dev] myapp-web-sg tcp/22, tcp/80, tcp/443
[prod] myapp-elb-sg tcp/80, icmp/3-4
myapp-bastion-sg tcp/22
prod のみ: ['icmp/3-4'] / 共通: tcp/22, tcp/80, tcp/443
9. この差分は「なぜ問題なのか」——出た差分の読み方
差分は出して終わりではありません。それぞれに本番リスクの意味があります。
| 検知される差分 | なぜ危険か |
|---|---|
| Prod の SG が 22/tcp を 0.0.0.0/0 公開 | SSH が全世界露出。総当たり・鍵漏洩で即侵入。Dev で塞いだなら Prod は戻し忘れの可能性大 |
| WAF の Core Rule Set が Prod に無い | SQLi/XSS など基礎攻撃が本番だけ素通り。Dev で検証した防御が効いていない |
| S3 versioning が Prod だけオフ | ランサムウェア暗号化・誤削除から復旧不能。Dev は守られているのに本番が裸 |
| EKS の public endpoint が Prod で有効 | K8s API サーバがインターネット露出。本番クラスタの管理面が攻撃対象に |
| EBS 暗号化が Prod だけ未適用 | 盤面・スナップショット流出時に平文。コンプラ違反にも直結 |
ポイントは、「Dev にあって Prod に無い(あるいは逆)」という非対称性そのものが赤信号だということ。
同じ穴が両方にあるなら「設計の方針」かもしれませんが、片側だけの差分は「事故・抜け漏れ」の確率が高い。
環境比較はこの非対称性を機械的に炙り出すのが本質的な価値です。
10. ネットワーク露出の差分
露出は 2 粒度で取れます。
- config レベル = 上の「0.0.0.0/0 を開けている SG ルール」(何が口を開けているか)
-
経路レベル =
isExposed(インターネット経路上にある資源)を Zone 別に集計
MATCH CloudResource IN Zone WHERE Zone.name =~ 'dev|prod' AND CloudResource.isExposed = true
RETURN Zone.name, CloudResource.type, count(DISTINCT CloudResource) AS exposed
ORDER BY Zone.name, exposed DESC
💡 検証コラム⑤:能動的な到達性検証はデータ依存
validatedExposure(実際に到達可能かを能動検証したフラグ)は、稼働ターゲットがある環境でのみ陽性になります。
ワークロード停止中の検証環境では全て false でした。「設定上どこが開いているか」「経路上どれが露出か」は静的に取れます。
11. ハマりどころまとめ
| # | ハマり | 結論 |
|---|---|---|
| ① | Zone をリソースタグで切る |
不可。labels=アカウントラベル。タグ絞りは Inventory 側 |
| ② |
expression ブロックを使う |
安定版は rules 文字列 |
| ③ | SysQL で生ルールを取る | 取れない。Config API が必要 |
| ④ | S3 の設定を bucket config で見る | versioning 等は別リソース型 |
| ⑤ |
validatedExposure 全 false |
データ依存。live ターゲットが要る |
| ⑥ | Zone をポリシースコープ/権限境界と誤解 | Zone はビュー絞り。範囲は Policy、権限は RBAC |
まとめ
- 環境ドリフトは「善意の運用」から生まれ、本番だけ弱くする。だから機械的・継続的な環境比較が要る。
- CNAPP(Sysdig)側で見ると、IaC 管理外の手動変更まで実態ベースで拾えるのが強み。
- Zone は「環境という論理境界をインベントリへ射影する」機構。これでスコープし、Config API で生ルールを読み、2環境を diff すれば、設定差分・露出差分が取れる。
- ただし Zone は万能ではない。ポリシースコープでもアクセス制御でもなく、AWS リソースタグでも切れない。役割を3軸(Zone / Policy / RBAC)に正しく振り分けるのが事故らないコツ。
同梱の Terraform とスクリプトで、あなたの環境でもそのまま試せます。
※リソース名・アカウント ID・ポート構成はすべてサニタイズ済みのサンプルです。
※Zone の論理モデルの詳細は同梱 ZONE-CONCEPT.md に。