Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions compliance/frameworks/cis_azure_benchmark.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/iso27001.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/nist_csf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/soc2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions playbooks/cli/fix_az_kv_006.sh
Original file line number Diff line number Diff line change
@@ -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> <vault-name>
RESOURCE_GROUP="${1:-}"
VAULT_NAME="${2:-}"
if [ -z "$RESOURCE_GROUP" ] || [ -z "$VAULT_NAME" ]; then
echo "Usage: $0 <resource-group> <vault-name>"
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 <principal-id> --scope <vault-resource-id>"
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"
58 changes: 58 additions & 0 deletions scanner/rules/az_kv_006.py
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions tests/test_rules_keyvault.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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"
Loading