From eed231acce5b147bcafa3b392869fa162d48aeef Mon Sep 17 00:00:00 2001 From: hannahwestra25 Date: Thu, 18 Jun 2026 15:28:49 -0400 Subject: [PATCH] fix(infra): eliminate KV public-access window for policy compliance Create Key Vault with defaultAction=Deny and only the deployer's IP allowlisted (instead of unrestricted public access). After the secret is written, remove the IP rule and disable publicNetworkAccess entirely. This eliminates the temporary violating state where the KV had publicNetworkAccess=Enabled, avoiding blocks from Azure policy enforcement that rejects resources created in a non-compliant state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/README.md | 7 ++--- infra/deploy_instance.py | 56 ++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/infra/README.md b/infra/README.md index 6971be2223..41a8329c8c 100644 --- a/infra/README.md +++ b/infra/README.md @@ -316,9 +316,10 @@ az keyvault show --name --query id -o tsv > **Note**: The vault should have `enableRbacAuthorization: true`. Diagnostic > settings (AuditEvent logs) should be configured separately by the vault -> owner. The deploy script applies `publicNetworkAccess=Disabled + -> defaultAction=Deny + bypass=AzureServices` after writing the backup secret -> (matches the team standard for SFI/NS221 compliance). +> owner. The deploy script creates the vault with `defaultAction=Deny` and only +> the deployer's IP allowlisted, then removes the IP rule and sets +> `publicNetworkAccess=Disabled` after writing the backup secret (matches the +> team standard for SFI/NS221 compliance — no window of unrestricted public access). ## Preview changes before deploying (recommended) diff --git a/infra/deploy_instance.py b/infra/deploy_instance.py index 737a7b9af6..9f6cb1a13a 100644 --- a/infra/deploy_instance.py +++ b/infra/deploy_instance.py @@ -635,11 +635,11 @@ def create_key_vault( - Provide a backup snapshot if the Container App is recreated. - Preserve the audit trail for the deployed configuration. - The vault is created with public network access enabled so that the - deployer (running from a corp network or dev container) can write the - initial secret. After the secret is uploaded, the vault is locked down - to publicNetworkAccess=Disabled + defaultAction=Deny + bypass=AzureServices - to satisfy the S360 / NS221 Secure PaaS alert. + The vault is created with network access restricted to the deployer's + public IP only (defaultAction=Deny + deployer IP allowlisted). After the + secret is uploaded, the deployer's IP rule is removed and public network + access is fully disabled to satisfy the S360 / NS221 Secure PaaS alert. + This avoids any window where the KV has unrestricted public access. Args: resource_group (str): The resource group name. @@ -651,7 +651,22 @@ def create_key_vault( Returns: str: The Key Vault resource ID. """ - logger.info("Creating Key Vault: %s", vault_name) + # Determine the deployer's public IP so we can allowlist only that IP + # instead of enabling unrestricted public access. + logger.info("Detecting deployer public IP for Key Vault network rules") + ip_result = subprocess.run( + ["curl", "-s", "https://ipinfo.io/ip"], + capture_output=True, + text=True, + check=True, + shell=_SHELL, + ) + deployer_ip = ip_result.stdout.strip() + if not deployer_ip: + raise RuntimeError("Failed to detect deployer public IP from https://ipinfo.io/ip") + logger.info("Deployer IP: %s", deployer_ip) + + logger.info("Creating Key Vault: %s (locked down, deployer IP allowlisted)", vault_name) kv_cmd = [ "keyvault", "create", @@ -665,6 +680,12 @@ def create_key_vault( "true", "--enable-purge-protection", "true", + "--default-action", + "Deny", + "--bypass", + "AzureServices", + "--network-acls-ips", + f"{deployer_ip}/32", ] if tags: kv_cmd += ["--tags"] + tags @@ -753,13 +774,24 @@ def create_key_vault( if tmp_path: Path(tmp_path).unlink(missing_ok=True) - # Apply SFI network lockdown after the backup secret is written. Safe - # because the Container App does NOT reference this KV at runtime - # (the .env is passed inline via Bicep's envFileContents @secure() param). + # Remove deployer IP allowlist and fully disable public network access. + # The KV was never publicly open — only the deployer's IP was allowed. # Matches the team standard observed on existing AIRT vaults # (e.g. airt-chatui-kv, AIRT-Blackhat-KV): publicNetworkAccess=Disabled, # default-deny ACL, bypass for trusted Azure services. - logger.info("Applying SFI network lockdown to Key Vault: %s", vault_name) + logger.info("Removing deployer IP rule from Key Vault: %s", vault_name) + run_az( + args=[ + "keyvault", + "network-rule", + "remove", + "--name", + vault_name, + "--ip-address", + f"{deployer_ip}/32", + ] + ) + logger.info("Disabling public network access on Key Vault: %s", vault_name) run_az( args=[ "keyvault", @@ -768,10 +800,6 @@ def create_key_vault( vault_name, "--public-network-access", "Disabled", - "--default-action", - "Deny", - "--bypass", - "AzureServices", ] )