構成の境界線 — 山崎行政書士事務所 パラメータ夜話
- 山崎行政書士事務所
- 2025年9月14日
- 読了時間: 6分

00:43アラートは、寝静まった事務所の天井を針で突くみたいに鳴った。[High sev] Key Vault: SecretGet from GitHub-hosted runner IP奏汰(そうた)がモニタに顔を寄せる。「prd-kv-01 の smtp-connector-password、公開ランナーのIPから読まれてる。ジョブのメタは repo:port-east/payment:ref:refs/heads/staging。」
律斗(りつと)が短く頷く。「ウォールーム、起こす。**“パラメータ会議”**だ。」
1|一次遮断は“値”から
00:51会議室A、ホワイトボードの左端にりなが書く。《遮断→証跡→運用差し替え》。悠真(ゆうま)がKQLを叩く。
ServicePrincipalSignInLogs
| where AppDisplayName == "github-oidc-sp"
| where TimeGenerated > ago(12h)
| summarize min(TimeGenerated), max(TimeGenerated), dcount(IPAddress), makeset(IPAddress)
by ServicePrincipalName, AppId
「github-oidc-sp が staging からも production からも来てる。」悠真。奏汰が苦い顔。「サービスプリンシパルのシークレットでログインしてた頃の名残、まだ生きてる。AZURE_CREDENTIALS のローテ忘れ…。」
りな:「まず“読み取りそのもの”を止める。Key Vaultの公開ネットワークを切る。つぎに“誰の権限”で読めたのかを詰める。最後に“どう運用を差し替えるか”。」
叶多(かなた)がTerraformのモジュールを開く。「差し替えパラメータ、貼るよ。」
# prd-kv-01 (Key Vault)
resource "azurerm_key_vault" "prd" {
name = "prd-kv-01"
resource_group_name = var.rg_name
location = var.location
sku_name = "premium"
soft_delete_retention_days = 90
purge_protection_enabled = true
public_network_access_enabled = false
minimum_tls_version = "TLS1_2"
network_acls {
bypass = "None"
default_action = "Deny"
ip_rules = [] # 全閉
virtual_network_subnet_ids = [azurerm_subnet.pe_subnet.id]
}
}
「public_network_access_enabled = false で即座に外からのGetは沈む。」蓮斗(れんと)が確認。「ただ、パイプラインが死ぬ。先にOIDCに乗り換えよう。」
2|シークレットからOIDCへ:主語はsubject
01:07奏汰がGitHub ActionsのYAMLを指で叩く。「id-token: write を付けて、SPのシークレットは捨てる。」
# .github/workflows/deploy.yml(抜粋)
permissions:
id-token: write
contents: read
env:
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/staging'
steps:
- uses: actions/checkout@v4
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
悠真:「でも誰のトークンでも入れるのは怖い。subject を固定する。」蓮斗:「repo:{owner}/{repo}:ref:refs/heads/staging しか受けない。」
りな:「NIS2の‘変更の管理’にも噛むわね。“どのブランチからの変更だけが入れるか”を規範化する。」
奏汰がTerraformの差分を流す。
# OIDCフェデレーション設定(AzureAD)
resource "azuread_application" "github_oidc_app" {
display_name = "github-oidc-sp"
}
resource "azuread_service_principal" "github_oidc_sp" {
application_id = azuread_application.github_oidc_app.application_id
}
resource "azuread_application_federated_identity_credential" "staging_cred" {
application_object_id = azuread_application.github_oidc_app.object_id
display_name = "repo-port-east-payment-staging"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:port-east/payment:ref:refs/heads/staging"
audiences = ["api://AzureADTokenExchange"]
}
律斗:「ロールは最小。Contributor は無し、Key Vault Secrets User と対象スコープを限定。」
resource "azurerm_role_assignment" "kv_secret_user_stg" {
scope = azurerm_key_vault.prd.id
role_definition_name = "Key Vault Secrets User"
principal_id = azuread_service_principal.github_oidc_sp.object_id
condition_version = "2.0"
# “このタグが 'staging' のときのみ許可”みたいな条件に拡張可
}
りな:「“本番Key Vaultに対してstagingブランチからのGetを禁止”、言い切れる文で運用通知を書く。」
3|ネットワークを絞る:Private Endpoint と DNS
01:28叶多:「Key VaultのPrivate Endpointを張る。Zoneは privatelink.vaultcore.azure.net。」
resource "azurerm_private_dns_zone" "kv" {
name = "privatelink.vaultcore.azure.net"
resource_group_name = var.rg_name
}
resource "azurerm_private_endpoint" "kv" {
name = "pe-prd-kv-01"
location = var.location
resource_group_name = var.rg_name
subnet_id = azurerm_subnet.pe_subnet.id
private_service_connection {
name = "kv-psc"
is_manual_connection = false
private_connection_resource_id = azurerm_key_vault.prd.id
subresource_names = ["vault"]
}
private_dns_zone_group {
name = "kv-dns"
private_dns_zone_ids = [azurerm_private_dns_zone.kv.id]
}
}
悠真:「サブネット側のPEポリシーは無効化済み?」叶多:「済み。ルートはFW経由。0.0.0.0/0 -> azfw。」りな:「“Key Vaultへは社内からだけ”、通知文に**“どこからでも”を消す**。」
4|“環境名”は呪文じゃない:TF_ENV を“実体化”する
01:46奏汰:「staging のつもりが prod に差す事故、変数の表記揺れが原因。TF_ENV をタグと名前に必ず混ぜる。」
variable "env" {
type = string
validation {
condition = contains(["dev", "stg", "prd"], var.env)
error_message = "env must be one of dev|stg|prd"
}
}
locals {
name = "port-${var.env}-kv-01"
tags = {
"Environment" = var.env
"Owner" = "LegalCloudOps"
"DataClass" = "C2"
}
}
蓮斗:「タグ Environment=stg 以外は受けないKey Vaultアクセス条件、次で切る。」りな:「例外には“期限”。stg が prd に触れるのは今夜の復旧ウィンドウだけ。」
5|可視化は“窓のサイズ”で決まる
02:05悠真がログの“窓”を調整する。「window_duration = PT5M、evaluation_frequency = PT1M。1分遅延で動く。」
# Azure Monitor Scheduled Query Alert (v2)
resource "azurerm_monitor_scheduled_query_rules_alert_v2" "sp_ip_spike" {
name = "sp-signin-unknown-ip"
resource_group_name = var.rg_name
location = var.location
evaluation_frequency = "PT1M"
window_duration = "PT5M"
severity = 2
scopes = [azurerm_log_analytics_workspace.law.id]
query = <<-KQL
ServicePrincipalSignInLogs
| where AppDisplayName == "github-oidc-sp"
| summarize cnt=dcount(IPAddress) by bin(TimeGenerated, 5m)
| where cnt > 1
KQL
action {
action_groups = [azurerm_monitor_action_group.ops.id]
}
}
「“5分で2IP以上”を“異常”にする。」悠真。りな:「誤検知の議論は明日。今夜は絞るのが先。」
6|読み取りの“証跡”は、テーブルに残す
02:24蓮斗がテーブルを切り替える。「KeyVaultDataPlane、SecretGet の列。」
KeyVaultDataPlane
| where ResourceId has "prd-kv-01"
| where OperationName == "SecretGet"
| project TimeGenerated, CallerIpAddress, Identity, ResultSignature, SecretName
| sort by TimeGenerated desc
「staging のジョブIDが紐づいてる。」蓮斗。りな:「“誰が・いつ・何を見たか”は事実。“漏えいしたか”は確認中。文の段落を分ける。」
7|CAポリシーは“誰が・どこから・何を”の三点
02:41叶多:「Azure管理面は準拠デバイスのみ+FIDO2。Cloud apps: Azure Management に絞る。」
{
"displayName": "CA-RequireCompliant-FIDO2-For-AzureMgmt",
"state": "enabled",
"conditions": {
"users": { "includeRoles": ["62e90394-69f5-4237-9190-012177145e10"] },
"applications": { "includeApplications": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"] },
"clientAppTypes": ["browser", "mobileAppsAndDesktopClients"]
},
"grantControls": {
"operator": "AND",
"builtInControls": ["mfa", "compliantDevice"]
},
"authenticationStrength": { "displayName": "Phishing-resistant MFA" }
}
りな:「“人の手の運用”はここまで。“機械の運用”はOIDCのsubjectで切る。二層で線を引く。」
8|“消す”勇気と“残す”設計
03:05奏汰:「GitHubのリポジトリシークレット、AZURE_CREDENTIALSを削除、ブランチ保護を厳格に。」
Required reviewers: 2
Dismiss stale approvals: On
Require status checks: tf-fmt, tf-plan, sec-scan
Require linear history: On
Force pushes: Block
Delete branches on merge: On
りな:「“削除”は“責任”。“戻せる証跡”も残す。tf-plan のアーティファクト保存**、90日に延長。」
9|“例外”の設計:期限はUTCで
03:22蓮斗:「例外許可:今から02:30–04:30 UTC。staging から prd Key Vaultの**SecretGetのみ**通す。」
# 期限付きロール(PIM化の前段)
resource "azurerm_role_assignment_schedule" "tmp_kv_reader" {
name = "tmp-stg-to-prd-kv-read"
scope = azurerm_key_vault.prd.id
principal_id = azuread_service_principal.github_oidc_sp.object_id
role_definition_name = "Key Vault Secrets User"
start_date_time = "2025-09-13T02:30:00Z"
expiration = "AfterDuration"
duration = "PT2H"
}
りな:「UTCで統一。“日本時間で…”は誤解を招く。」
10|夜明け前、線は閉じられる
04:06ServicePrincipalのサインインは収束、Key VaultのSecretGetは社内IPだけになった。奏汰が最後のコミットメッセージを書く。
chore: switch to OIDC; disable kv public; add PE & dns; tag env; alert v2
やまにゃんが会議室の隅でUSBのしっぽを揺らす札を置く。「みゃ〜てら!(Terraform)」
11|朝会:“言い切り”と“数字”
09:10ふみか(広報)が一次報文を読み上げる。
事象:本番Key Vaultのシークレット読み取りに外部IPからのアクセスを検知。
対応:公開ネットワーク遮断(public_network_access_enabled=false)、Private Endpoint導入、GitHub OIDC移行(subject固定)、期限付き例外で復旧を完了。
影響:読み取りは確認。第三者提供の確証は無し(継続調査)。
再発防止:タグ基準のアクセス制御(Environment=stg 不可)、Scheduled Query Alert(1分×5分窓)、ブランチ保護強化。
りな:「“確認”と“未確定”を同じ段で書かない。数字(PT5M、PT1M、90日、UTC)は必ず添える。」
律斗:「パラメータで守った夜だ。設計は物語になる。今日のKPT、Kに“値で止めた”と残す。」
12|Appendix(内部メモ/抜粋)
A. KQL(調査用)
// OIDCトークンのsubject検証
ServicePrincipalSignInLogs
| where AppDisplayName == "github-oidc-sp"
| project TimeGenerated, IPAddress, ClientAppUsed, TokenIssuerType, Claims = tostring(parse_json(AuthenticationDetails)[0].authenticationRequirement)
// Key Vaultデータプレーンの追跡
KeyVaultDataPlane
| where OperationName in ("SecretGet","SecretList")
| summarize makeset(SecretName), min(TimeGenerated), max(TimeGenerated) by Identity
B. Terraform(安全側のデフォルト)
# Storage Account(念押し)
resource "azurerm_storage_account" "prd" {
name = "portprdstore01"
resource_group_name = var.rg_name
location = var.location
account_tier = "Standard"
account_replication_type = "ZRS"
min_tls_version = "TLS1_2"
public_network_access_enabled = false
allow_blob_public_access = false
shared_access_key_enabled = false
tags = local.tags
}
C. 監視ルールの設計指針
窓(window_duration)は5〜10分、頻度(evaluation_frequency)は1分。
信号の粒度はServicePrincipalSignInLogsとKeyVaultDataPlaneを別アラートに。
しきい値は**“2種類以上のIP”や“1分間に3回以上のSecretGet”の行動**で定義する。
付記:この物語はフィクションですが、登場するパラメータや設定例は実務で用いられる粒度を想定しています。技術は“値”で動き、“値”で止まる。 そして文章が、その“値”に意味を与え、現場を同じ方向に向けます。今日は、その三つが噛み合った夜でした。



コメント