diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index 25146ec..d6a5ec4 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -198,6 +198,11 @@ "control_name": "Ensure that the expiration date is set on all certificates", "description": "A certificate stored in Azure Key Vault is expiring within 30 days and does not have auto-renewal configured. CIS 8.5 requires that expiration dates are monitored and certificates are renewed before expiry to prevent service outages and broken authentication flows." }, + "AZ-KV-006": { + "control_id": "TBD-KV-006", + "control_name": "Ensure that Azure Key Vault Uses Azure RBAC for Data Plane Authorization", + "description": "Key Vaults authorizing access through legacy vault access policies instead of Azure RBAC lack scoped, auditable role assignments. Access policies grant broad permissions per permission type and are not tracked through Azure RBAC's centralized role-assignment audit trail, increasing the risk of over-privileged access to secrets, keys, and certificates. Note for maintainers: the official CIS Azure Foundations Benchmark control for this check is 8.6 (\"Enable Role Based Access Control for Azure Key Vault\"), but 8.6 is already assigned to AZ-KV-004 (purge protection) in this file — likely a pre-existing mapping error unrelated to this change. Left as TBD pending a maintainer decision on how to resolve the collision, following the same TBD-* convention used elsewhere in this file." + }, "AZ-NET-013": { "control_id": "6.4", "control_name": "Ensure that Azure Firewall is enabled on Virtual Networks", diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index 0ab3c86..9cfb9fa 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -188,6 +188,11 @@ "control_name": "Key management", "description": "A certificate stored in Azure Key Vault is expiring within 30 days with no auto-renewal configured. A.10.1.2 requires that a policy on the use, protection, and lifetime of cryptographic keys is developed and implemented. Certificates approaching expiry without renewal represent a failure in cryptographic key lifecycle management." }, + "AZ-KV-006": { + "control_id": "A.9.2.3", + "control_name": "Management of privileged access rights", + "description": "Key Vaults authorizing access through legacy vault access policies instead of Azure RBAC lack scoped, reviewable privileged-access management. A.9.2.3 requires that the allocation of privileged access rights is restricted and controlled. Access policies do not provide the granular, role-based control needed to enforce least privilege on secrets, keys, and certificates." + }, "AZ-DB-004": { "control_id": "A.13.1.1", "control_name": "Network controls", diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index 69167b3..20a6806 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -188,6 +188,11 @@ "control_name": "Maintenance and repair of organisational assets is performed", "description": "A certificate stored in Azure Key Vault is expiring within 30 days with no auto-renewal configured. PR.MA-1 requires that maintenance of organisational assets is performed and logged. Certificate renewal is a critical maintenance task and failure to renew before expiry causes immediate service disruption." }, + "AZ-KV-006": { + "control_id": "PR.AC-4", + "control_name": "Access permissions and authorizations are managed", + "description": "Key Vaults authorizing access through legacy vault access policies instead of Azure RBAC lack centrally managed, auditable access permissions. PR.AC-4 requires that access permissions are managed incorporating the principles of least privilege and separation of duties. Access policies cannot express fine-grained, role-scoped permissions the way Azure RBAC role assignments can." + }, "AZ-DB-004": { "control_id": "PR.AC-3", "control_name": "Remote access is managed", diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index ba19486..2e8af6f 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -188,6 +188,11 @@ "control_name": "Risk Mitigation", "description": "A certificate stored in Azure Key Vault is expiring within 30 days with no auto-renewal configured. CC9.1 requires that identified risks are mitigated through controls that reduce the likelihood or impact of risk events. An expiring certificate without auto-renewal represents an unmitigated operational risk that will cause service outages if not addressed." }, + "AZ-KV-006": { + "control_id": "CC6.1", + "control_name": "Logical Access Security", + "description": "Key Vaults authorizing access through legacy vault access policies instead of Azure RBAC lack the scoped, role-based logical access controls CC6.1 requires. Access policies grant broad, per-permission-type access rather than least-privilege role assignments, increasing the risk of unauthorized access to secrets, keys, and certificates." + }, "AZ-DB-004": { "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", diff --git a/playbooks/cli/fix_az_kv_006.sh b/playbooks/cli/fix_az_kv_006.sh new file mode 100644 index 0000000..61336cb --- /dev/null +++ b/playbooks/cli/fix_az_kv_006.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail +# AZ-KV-006: Switch an Azure Key Vault from access policies to Azure RBAC authorization +# Usage: ./fix_az_kv_006.sh +RESOURCE_GROUP="${1:-}" +VAULT_NAME="${2:-}" +if [ -z "$RESOURCE_GROUP" ] || [ -z "$VAULT_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi +echo "WARNING: switching Key Vault '$VAULT_NAME' to RBAC authorization stops enforcing" +echo "its existing access policies. Assign equivalent RBAC roles BEFORE proceeding," +echo "or callers relying on those access policies will lose access, e.g.:" +echo " az role assignment create --role \"Key Vault Secrets User\" \\" +echo " --assignee --scope " +echo "Enabling RBAC authorization on Key Vault: $VAULT_NAME..." +az keyvault update \ + --resource-group "$RESOURCE_GROUP" \ + --name "$VAULT_NAME" \ + --enable-rbac-authorization true +echo "RBAC authorization enabled for Key Vault: $VAULT_NAME" diff --git a/scanner/rules/az_kv_006.py b/scanner/rules/az_kv_006.py new file mode 100644 index 0000000..0492319 --- /dev/null +++ b/scanner/rules/az_kv_006.py @@ -0,0 +1,58 @@ +"""AZ-KV-006: Key Vault using legacy access policies instead of Azure RBAC.""" + +from typing import Any, Dict, List + +RULE_ID = "AZ-KV-006" +RULE_NAME = "Key Vault Using Legacy Access Policies Instead of Azure RBAC" +SEVERITY = "MEDIUM" +CATEGORY = "KeyVault" +FRAMEWORKS = {"CIS": "TBD-KV-006", "NIST": "PR.AC-4", "ISO27001": "A.9.2.3", "SOC2": "CC6.1"} +DESCRIPTION = ( + "The Azure Key Vault is authorizing access through legacy vault access policies " + "instead of Azure RBAC. Access policies are all-or-nothing per permission type, " + "cannot be scoped to individual keys/secrets/certificates, are not covered by " + "Azure RBAC's centralized audit trail (Activity Log role assignments), and are " + "easy to over-grant since there is no built-in least-privilege role model." +) +REMEDIATION = ( + "Enable Azure RBAC authorization on the Key Vault and replace access policies " + "with scoped role assignments (e.g. Key Vault Secrets User, Key Vault Crypto Officer). " + "Note: switching to RBAC does not delete existing access policies, but they stop being enforced." +) +PLAYBOOK = "playbooks/cli/fix_az_kv_006.sh" + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Detect Key Vaults where enable_rbac_authorization is False or None.""" + findings: List[Dict[str, Any]] = [] + + for vault in azure_client.get_key_vaults(): + props = getattr(vault, "properties", None) + if props is None: + continue + + # Access policies are the legacy default; a vault must opt into RBAC. + rbac_enabled = getattr(props, "enable_rbac_authorization", False) + if not rbac_enabled: + parsed = azure_client.parse_resource_id(vault.id) + findings.append( + { + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": vault.id, + "resource_name": vault.name, + "resource_type": "Microsoft.KeyVault/vaults", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": parsed.get("resource_group", ""), + "location": getattr(vault, "location", ""), + }, + } + ) + + return findings diff --git a/tests/test_rules_keyvault.py b/tests/test_rules_keyvault.py index 538bede..67704df 100644 --- a/tests/test_rules_keyvault.py +++ b/tests/test_rules_keyvault.py @@ -11,6 +11,7 @@ import scanner.rules.az_kv_003 as az_kv_003 import scanner.rules.az_kv_004 as az_kv_004 import scanner.rules.az_kv_005 as az_kv_005 +import scanner.rules.az_kv_006 as az_kv_006 from tests.helpers.mock_azure import make_resource _REQUIRED_FIELDS = { @@ -177,3 +178,30 @@ def test_kv_005_noncompliant_expiring_soon_returns_one_finding(mock_azure, subsc assert findings[0]["rule_id"] == "AZ-KV-005" assert findings[0]["severity"] == "MEDIUM" assert findings[0]["resource_name"] == "cert-expiring" + + +# ── AZ-KV-006: legacy access policies instead of Azure RBAC ──────────────── + + +def test_kv_006_compliant_rbac_enabled_returns_no_findings(mock_azure, subscription_id): + mock_azure.set_key_vaults([_vault_with_props("kv-rbac-on", enable_rbac_authorization=True)]) + assert az_kv_006.scan(mock_azure, subscription_id) == [] + + +def test_kv_006_noncompliant_access_policies_returns_one_finding(mock_azure, subscription_id): + mock_azure.set_key_vaults([_vault_with_props("kv-rbac-off", enable_rbac_authorization=False)]) + findings = az_kv_006.scan(mock_azure, subscription_id) + assert len(findings) == 1 + assert _REQUIRED_FIELDS.issubset(findings[0].keys()) + assert findings[0]["rule_id"] == "AZ-KV-006" + assert findings[0]["severity"] == "MEDIUM" + assert findings[0]["resource_name"] == "kv-rbac-off" + assert findings[0]["metadata"]["resource_group"] == _RG + + +def test_kv_006_missing_property_defaults_to_noncompliant(mock_azure, subscription_id): + """A vault with no enable_rbac_authorization attribute is legacy access-policy by default.""" + mock_azure.set_key_vaults([_vault_with_props("kv-rbac-unset")]) + findings = az_kv_006.scan(mock_azure, subscription_id) + assert len(findings) == 1 + assert findings[0]["rule_id"] == "AZ-KV-006"