Skip to main content
Pulumi logo Pulumi logo
  1. Docs
  2. Reference
  3. Pre-built Policy Packs
  4. CIS Kubernetes
  5. Azure (AKS)

CIS Kubernetes - Azure (AKS)

    This page lists all 30 policies in the CIS Kubernetes pack for Azure (AKS), as published in cis-kubernetes-azure version 1.0.1.

    Policies by control

    2.1 Audit Logging — Enable audit logs for AKS clusters to track all API server requests and administrative actions.

    4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.

    4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.

    4.4 Network Policies — Ensure that all Namespaces have Network Policies defined.

    4.5-4.6 Secret Management and Namespaces — Prefer using secrets as files over secrets as environment variables. Ensure default namespace is not used for workloads. Apply security context to pods and containers.

    5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).

    5.2 Service Accounts — Prefer using dedicated AKS Service Accounts.

    5.3 Encryption — Ensure Kubernetes Secrets are encrypted.

    5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.

    5.5 Azure AD Integration — Manage Kubernetes RBAC users with Azure AD. Use Azure RBAC for Kubernetes Authorization.

    Policy details

    acr-user-access-minimized

    Severity: medium · Enforcement: advisory

    Ensure Azure Container Registry has minimized user access with proper authentication and network restrictions.

    • 5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).
    Remediation
    Fix: Minimize User Access to Azure Container Registry
    Step 1: Disable Admin User and Use Service Principals
    import * as azuread from "@pulumi/azuread";
    
    // Create service principal for ACR access
    const acrServicePrincipal = new azuread.ServicePrincipal("acr-sp", {
        applicationId: acrApp.applicationId,
    });
    
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "myregistry",
        sku: {
            name: "Premium",  // Premium SKU for advanced features
        },
        adminUserEnabled: false,  // Disable admin user
        publicNetworkAccess: "Disabled",  // Disable public access
        anonymousPullEnabled: false,  // Disable anonymous pull
        // ... other config
    });
    
    // Grant AcrPull role to service principal
    const acrPullRole = new azure.authorization.RoleAssignment("acr-pull", {
        principalId: acrServicePrincipal.objectId,
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d", // AcrPull
        scope: registry.id,
    });
    
    Step 2: Configure Private Endpoints
    const privateEndpoint = new azure.network.PrivateEndpoint("acr-pe", {
        resourceGroupName: resourceGroup.name,
        location: resourceGroup.location,
        subnet: {
            id: privateSubnet.id,
        },
        privateLinkServiceConnections: [{
            name: "acr-connection",
            privateLinkServiceId: registry.id,
            groupIds: ["registry"],
        }],
    });
    
    const privateDnsZone = new azure.network.PrivateZone("acr-dns", {
        resourceGroupName: resourceGroup.name,
        privateZoneName: "privatelink.azurecr.io",
    });
    
    Step 3: Configure Network Rules for Limited Public Access (if required)
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "myregistry",
        sku: {
            name: "Premium",
        },
        adminUserEnabled: false,
        publicNetworkAccess: "Enabled",  // Enable with restrictions
        networkRuleSet: {
            defaultAction: "Deny",  // Deny by default
            ipRules: [
                {
                    action: "Allow",
                    ipAddressOrRange: "203.0.113.0/24",  // Office network
                },
                {
                    action: "Allow",
                    ipAddressOrRange: "198.51.100.0/24",  // VPN range
                },
            ],
            virtualNetworkRules: [
                {
                    action: "Allow",
                    virtualNetworkResourceId: subnet.id,
                },
            ],
        },
        // ... other config
    });
    
    Step 4: Use Managed Identity for AKS Access
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        identity: {
            type: "SystemAssigned",
        },
        // ... other config
    });
    
    // Grant AcrPull to AKS managed identity
    const acrPullForAKS = new azure.authorization.RoleAssignment("aks-acr-pull", {
        principalId: cluster.identity.apply(i => i!.principalId),
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d", // AcrPull
        scope: registry.id,
    });
    

    Azure Container Registry Roles:

    • AcrPull: Pull images only
    • AcrPush: Pull and push images
    • AcrDelete: Pull, push, and delete images
    • AcrImageSigner: Sign images
    • Owner: Full control over registry

    aks-acr-readonly-access

    Severity: medium · Enforcement: advisory

    Ensure AKS clusters have read-only access to Azure Container Registry, preventing unauthorized image pushes.

    • 5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).
    Remediation
    Fix: Configure Read-Only ACR Access for AKS
    Step 1: Remove Excessive Permissions
    ##### Remove existing role assignments with push permissions
    az role assignment delete \
        --assignee <aks-managed-identity-object-id> \
        --role AcrPush \
        --scope <acr-resource-id>
    
    az role assignment delete \
        --assignee <aks-managed-identity-object-id> \
        --role Contributor \
        --scope <acr-resource-id>
    
    Step 2: Grant AcrPull (Read-Only) Access to AKS
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        identity: {
            type: "SystemAssigned",
        },
        // ... other config
    });
    
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "myregistry",
        sku: {
            name: "Premium",
        },
        // ... other config
    });
    
    // Grant read-only access (AcrPull) to AKS cluster
    const acrPullRole = new azure.authorization.RoleAssignment("aks-acr-pull", {
        principalId: cluster.identity.apply(i => i!.principalId!),
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d", // AcrPull
        scope: registry.id,
    });
    
    Step 3: Use Separate Service Principal for CI/CD Push
    import * as azuread from "@pulumi/azuread";
    
    // Create service principal for CI/CD pipelines (push access)
    const cicdApp = new azuread.Application("cicd-app", {
        displayName: "ACR Push CI/CD",
    });
    
    const cicdSp = new azuread.ServicePrincipal("cicd-sp", {
        applicationId: cicdApp.applicationId,
    });
    
    const cicdPassword = new azuread.ServicePrincipalPassword("cicd-password", {
        servicePrincipalId: cicdSp.id,
    });
    
    // Grant push access only to CI/CD service principal
    const acrPushRole = new azure.authorization.RoleAssignment("cicd-acr-push", {
        principalId: cicdSp.objectId,
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/8311e382-0749-4cb8-b61a-304f252e45ec", // AcrPush
        scope: registry.id,
    });
    
    Step 4: Use Attachment for ACR Integration
    // Alternatively, use attachACR for simplified setup
    const acrAttachment = new azure.containerservice.ManagedClusterAcrAttachment("acr-attachment", {
        resourceGroupName: resourceGroup.name,
        resourceName: cluster.name,
        acrName: registry.name,
    });
    

    Azure Container Registry Roles:

    • AcrPull (7f951dda-4ed3-4680-a7ca-43fe172d538d): Pull images only - recommended for AKS
    • AcrPush (8311e382-0749-4cb8-b61a-304f252e45ec): Pull and push images - use only for CI/CD
    • AcrDelete (c2f4ef07-c644-48eb-af81-4b1b4947fb11): Pull, push, and delete - use sparingly
    • Contributor: Full control - avoid for container operations

    Best Practices:

    • AKS clusters should only have AcrPull (read-only) access
    • Use separate service principals/managed identities for CI/CD push operations
    • Audit role assignments regularly
    • Use Azure Policy to enforce ACR access controls

    aks-approved-registries-only

    Severity: high · Enforcement: advisory

    Ensure Kubernetes workloads only pull container images from approved registries to prevent supply chain attacks.

    • 5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).
    Remediation
    Fix: Restrict Container Images to Approved Registries
    Step 1: Define Approved Registry List
    // Configure policy with approved registries
    const policyConfig = {
        "aks-approved-registries-only": {
            approvedRegistries: [
                "mycompany.azurecr.io",           // Corporate ACR
                "production.azurecr.io",          // Production ACR
                "gcr.io/my-project",              // GCP registry if needed
                "mycompany.io",                   // Custom domain
            ],
            allowSameSubscriptionACR: true,       // Allow *.azurecr.io
            requireFullyQualifiedImages: true,    // Prevent :latest tags
            allowDevNamespaces: ["dev", "test"],  // Dev exemptions
        },
    };
    
    Step 2: Deploy Applications Using Approved Registries
    import * as k8s from "@pulumi/kubernetes";
    
    const deployment = new k8s.apps.v1.Deployment("my-app", {
        metadata: {
            name: "my-app",
        },
        spec: {
            replicas: 3,
            selector: {
                matchLabels: { app: "my-app" },
            },
            template: {
                metadata: {
                    labels: { app: "my-app" },
                },
                spec: {
                    containers: [{
                        name: "app",
                        // GOOD: Using approved ACR registry with specific tag
                        image: "mycompany.azurecr.io/my-app:v1.2.3",
                        ports: [{ containerPort: 8080 }],
                    }],
                },
            },
        },
    });
    
    // BAD examples (will fail policy):
    // image: "docker.io/untrusted/image:latest"      // Unapproved registry
    // image: "mycompany.azurecr.io/my-app:latest"    // Using :latest tag
    // image: "mycompany.azurecr.io/my-app"           // Implicit :latest
    
    Step 3: Use Image Pull Secrets for Private Registries
    import * as k8s from "@pulumi/kubernetes";
    import * as pulumi from "@pulumi/pulumi";
    
    // Create Docker config secret for ACR authentication
    const dockerConfig = pulumi.secret({
        auths: {
            "mycompany.azurecr.io": {
                username: "<service-principal-id>",
                password: "<service-principal-password>",
                email: "notused@example.com",
                auth: "<base64-encoded-username:password>",
            },
        },
    });
    
    const acrSecret = new k8s.core.v1.Secret("acr-secret", {
        metadata: {
            name: "acr-secret",
            namespace: "default",
        },
        type: "kubernetes.io/dockerconfigjson",
        stringData: {
            ".dockerconfigjson": dockerConfig.apply(c => JSON.stringify(c)),
        },
    });
    
    // Deployment using the image pull secret
    const deployment = new k8s.apps.v1.Deployment("my-app", {
        metadata: { name: "my-app" },
        spec: {
            replicas: 3,
            selector: { matchLabels: { app: "my-app" } },
            template: {
                metadata: { labels: { app: "my-app" } },
                spec: {
                    imagePullSecrets: [{ name: acrSecret.metadata.name }],
                    containers: [{
                        name: "app",
                        image: "mycompany.azurecr.io/my-app:v1.2.3",
                    }],
                },
            },
        },
    });
    
    import * as azure from "@pulumi/azure-native";
    
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        identity: {
            type: "SystemAssigned",
        },
        // ... other config
    });
    
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "mycompany",
        sku: { name: "Premium" },
    });
    
    // Grant AKS pull access to ACR
    const acrPullRole = new azure.authorization.RoleAssignment("aks-acr-pull", {
        principalId: cluster.identity.apply(i => i!.principalId!),
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d", // AcrPull
        scope: registry.id,
    });
    

    Best Practices:

    • Maintain a corporate list of approved registries
    • Use ACR with Azure AD integration for authentication
    • Scan images in approved registries with Defender for Containers
    • Use image signing and verification (e.g., Notary, Cosign)
    • Avoid :latest tags; use specific version tags
    • Implement admission controllers (Gatekeeper, Kyverno) for enforcement
    • Regularly audit images in use across clusters

    aks-azure-ad-integration-enabled

    Severity: high · Enforcement: advisory

    Ensure AKS clusters are integrated with Azure AD for centralized user authentication and management.

    • 5.5 Azure AD Integration — Manage Kubernetes RBAC users with Azure AD. Use Azure RBAC for Kubernetes Authorization.
    Remediation
    Fix: Enable Azure AD Integration for AKS Cluster
    Step 1: Create Azure AD Groups for Cluster Administration
    import * as azuread from "@pulumi/azuread";
    
    const clusterAdminsGroup = new azuread.Group("aks-cluster-admins", {
        displayName: "AKS Cluster Admins",
        securityEnabled: true,
        description: "Administrators for AKS clusters",
    });
    
    const clusterUsersGroup = new azuread.Group("aks-cluster-users", {
        displayName: "AKS Cluster Users",
        securityEnabled: true,
        description: "Users for AKS clusters",
    });
    
    Step 2: Configure AKS with Azure AD Integration
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        aadProfile: {
            managed: true,  // Enable managed Azure AD integration
            enableAzureRBAC: false,  // Use Kubernetes RBAC (or true for Azure RBAC)
            adminGroupObjectIDs: [
                clusterAdminsGroup.objectId,  // Azure AD group for cluster admins
            ],
            tenantID: tenantId,  // Optional: specify tenant ID
        },
        disableLocalAccounts: true,  // Disable local accounts for security
        // ... other config
    });
    
    Step 3: Configure Azure AD with Azure RBAC (Optional - Enhanced Security)
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        aadProfile: {
            managed: true,
            enableAzureRBAC: true,  // Use Azure RBAC for Kubernetes authorization
            adminGroupObjectIDs: [
                clusterAdminsGroup.objectId,
            ],
        },
        disableLocalAccounts: true,
        // ... other config
    });
    
    // Assign Azure roles for Kubernetes access
    const adminRoleAssignment = new azure.authorization.RoleAssignment("aks-admin-role", {
        principalId: clusterAdminsGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8", // Azure Kubernetes Service Cluster Admin Role
        scope: cluster.id,
    });
    

    Important Notes:

    • Managed Azure AD is the recommended approach (legacy AAD is deprecated)
    • Disabling local accounts ensures all access goes through Azure AD
    • Consider using Azure RBAC for Kubernetes for centralized access management
    • Users need appropriate Azure AD group membership to access the cluster

    aks-azure-rbac-enabled

    Severity: high · Enforcement: advisory

    Ensure AKS clusters use Azure RBAC for Kubernetes authorization, providing centralized access control.

    • 5.5 Azure AD Integration — Manage Kubernetes RBAC users with Azure AD. Use Azure RBAC for Kubernetes Authorization.
    Remediation
    Fix: Enable Azure RBAC for Kubernetes Authorization

    Note: Azure RBAC requires managed Azure AD integration. Ensure the aks-azure-ad-integration-enabled policy is satisfied first.

    Step 1: Enable Azure RBAC on AKS Cluster
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        aadProfile: {
            managed: true,  // Managed Azure AD integration is required
            enableAzureRBAC: true,  // Enable Azure RBAC for Kubernetes
            adminGroupObjectIDs: [
                clusterAdminsGroup.objectId,
            ],
            tenantID: tenantId,
        },
        disableLocalAccounts: true,  // Recommended with Azure RBAC
        // ... other config
    });
    
    Step 2: Assign Azure Built-in Roles for Kubernetes Access
    // Cluster Admin Role
    const clusterAdminRole = new azure.authorization.RoleAssignment("cluster-admin", {
        principalId: adminGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8", // Azure Kubernetes Service Cluster Admin Role
        scope: cluster.id,
    });
    
    // Cluster User Role
    const clusterUserRole = new azure.authorization.RoleAssignment("cluster-user", {
        principalId: userGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/4abbcc35-e782-43d8-92c5-2d3f1bd2253f", // Azure Kubernetes Service Cluster User Role
        scope: cluster.id,
    });
    
    // RBAC Admin (manage roles and bindings)
    const rbacAdminRole = new azure.authorization.RoleAssignment("rbac-admin", {
        principalId: rbacAdminGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/3498e952-d568-435e-9b2c-8d77e338d7f7", // Azure Kubernetes Service RBAC Admin
        scope: cluster.id,
    });
    
    // RBAC Reader (view resources)
    const rbacReaderRole = new azure.authorization.RoleAssignment("rbac-reader", {
        principalId: readerGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/7f6c6a51-bcf8-42ba-9220-52d62157d7db", // Azure Kubernetes Service RBAC Reader
        scope: cluster.id,
    });
    
    // RBAC Writer (modify resources)
    const rbacWriterRole = new azure.authorization.RoleAssignment("rbac-writer", {
        principalId: writerGroup.objectId,
        principalType: "Group",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/a7ffa36f-339b-4b5c-8bfd-d2d2f9b8018d", // Azure Kubernetes Service RBAC Writer
        scope: cluster.id,
    });
    
    Step 3: Create Custom Role for Specific Permissions (Optional)
    const customK8sRole = new azure.authorization.RoleDefinition("custom-k8s-role", {
        roleName: "AKS Custom Developer",
        description: "Custom role for developers with limited Kubernetes permissions",
        assignableScopes: [subscription.id],
        permissions: [{
            actions: [
                "Microsoft.ContainerService/managedClusters/listClusterUserCredential/action",
                "Microsoft.ContainerService/managedClusters/read",
            ],
            dataActions: [
                "Microsoft.ContainerService/managedClusters/apps/deployments/write",
                "Microsoft.ContainerService/managedClusters/apps/deployments/read",
                "Microsoft.ContainerService/managedClusters/core/pods/read",
                "Microsoft.ContainerService/managedClusters/core/pods/logs/read",
            ],
        }],
    });
    
    const customRoleAssignment = new azure.authorization.RoleAssignment("custom-role-assignment", {
        principalId: devGroup.objectId,
        principalType: "Group",
        roleDefinitionId: customK8sRole.id,
        scope: cluster.id,
    });
    

    Azure Built-in Kubernetes Roles:

    • Azure Kubernetes Service Cluster Admin: Full access to all resources
    • Azure Kubernetes Service Cluster User: List cluster credentials, basic access
    • Azure Kubernetes Service RBAC Admin: Manage RBAC permissions
    • Azure Kubernetes Service RBAC Writer: Create/update most resources
    • Azure Kubernetes Service RBAC Reader: View most resources
    • Azure Kubernetes Service RBAC Cluster Admin: Full admin within namespaces

    aks-cluster-audit-logging-enabled

    Severity: high · Enforcement: advisory

    Ensure AKS clusters have audit logging enabled via diagnostic settings for security monitoring and compliance.

    • 2.1 Audit Logging — Enable audit logs for AKS clusters to track all API server requests and administrative actions.
    Remediation
    Fix: Enable Audit Logging for AKS Cluster
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        addonProfiles: {
            omsagent: {
                enabled: true,
                config: {
                    logAnalyticsWorkspaceResourceID: workspace.id,
                },
            },
        },
        // ... other config
    });
    
    // Also create diagnostic settings for detailed logs
    const diagnosticSetting = new azure.insights.DiagnosticSetting("clusterDiagnostics", {
        resourceUri: cluster.id,
        workspaceId: workspace.id,
        logs: [
            { category: "kube-apiserver", enabled: true },
            { category: "kube-audit", enabled: true },
            { category: "kube-audit-admin", enabled: true },
            { category: "kube-controller-manager", enabled: true },
            { category: "kube-scheduler", enabled: true },
            { category: "cluster-autoscaler", enabled: true },
            { category: "guard", enabled: true },
        ],
    });
    

    aks-cluster-endpoint-restrict-public-access

    Severity: high · Enforcement: advisory

    Ensure AKS cluster API server has restricted public access through private endpoints or authorized IP ranges.

    • 5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.
    Remediation
    Fix: Restrict API Server Access for AKS Cluster
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: true,
            enablePrivateClusterPublicFQDN: false, // Disable public FQDN
            privateDNSZone: "system", // Or specify custom private DNS zone ID
        },
        // ... other config
    });
    
    Option 2: Restrict to Specific IP Ranges
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: false,
            authorizedIPRanges: [
                "203.0.113.0/24",    // Office network
                "198.51.100.0/24",   // VPN range
                "192.0.2.1/32",      // Specific management IP
            ],
        },
        // ... other config
    });
    
    Option 3: Private Cluster with Limited Public Access
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: true,
            enablePrivateClusterPublicFQDN: true, // Enable public FQDN for specific scenarios
            privateDNSZone: privateZone.id,
            authorizedIPRanges: [
                "203.0.113.0/24", // Backup access from specific network
            ],
        },
        // ... other config
    });
    

    Best Practices:

    • Use private clusters for production environments
    • If public access is required, always restrict to specific IP ranges
    • Regularly review and update authorized IP ranges
    • Use Azure Firewall or Network Security Groups for additional protection

    aks-dedicated-service-accounts

    Severity: medium · Enforcement: advisory

    Ensure Kubernetes workloads use dedicated service accounts instead of the default service account for better access control and auditability.

    • 5.2 Service Accounts — Prefer using dedicated AKS Service Accounts.
    Remediation
    Fix: Use Dedicated Service Accounts for Workloads
    import * as azuread from "@pulumi/azuread";
    import * as azure from "@pulumi/azure-native";
    
    // Create Azure AD app and federated credential
    const app = new azuread.Application("app", {
        displayName: "my-app-identity",
    });
    
    const sp = new azuread.ServicePrincipal("app-sp", {
        applicationId: app.applicationId,
    });
    
    const federatedIdentity = new azuread.ApplicationFederatedIdentityCredential("federated-id", {
        applicationObjectId: app.objectId,
        displayName: "my-app-k8s-federation",
        audiences: ["api://AzureADTokenExchange"],
        issuer: cluster.oidcIssuerProfile.apply(p => p!.issuerUrl!),
        subject: "system:serviceaccount:production:my-app-sa",
    });
    
    // Create service account with workload identity annotation
    const appServiceAccount = new k8s.core.v1.ServiceAccount("app-sa", {
        metadata: {
            name: "my-app-sa",
            namespace: "production",
            annotations: {
                "azure.workload.identity/client-id": app.applicationId,
            },
            labels: {
                "azure.workload.identity/use": "true",
            },
        },
    });
    
    // Grant Azure permissions
    const roleAssignment = new azure.authorization.RoleAssignment("app-role", {
        principalId: sp.objectId,
        principalType: "ServicePrincipal",
        roleDefinitionId: "/providers/Microsoft.Authorization/roleDefinitions/2a2b9908-6ea1-4ae2-8e65-a410df84e7d1", // Storage Blob Data Reader
        scope: storageAccount.id,
    });
    
    Alternative: Basic Service Account (for K8s API access only)
    import * as k8s from "@pulumi/kubernetes";
    
    // Create dedicated service account
    const appServiceAccount = new k8s.core.v1.ServiceAccount("app-sa", {
        metadata: {
            name: "my-app-sa",
            namespace: "production",
        },
        automountServiceAccountToken: false,  // Only mount if needed
    });
    
    // Optional: Add RBAC permissions
    const appRole = new k8s.rbac.v1.Role("app-role", {
        metadata: {
            name: "my-app-role",
            namespace: "production",
        },
        rules: [{
            apiGroups: [""],
            resources: ["configmaps"],
            verbs: ["get", "list"],
        }],
    });
    
    const appRoleBinding = new k8s.rbac.v1.RoleBinding("app-binding", {
        metadata: {
            name: "my-app-binding",
            namespace: "production",
        },
        subjects: [{
            kind: "ServiceAccount",
            name: appServiceAccount.metadata.name,
            namespace: "production",
        }],
        roleRef: {
            kind: "Role",
            name: appRole.metadata.name,
            apiGroup: "rbac.authorization.k8s.io",
        },
    });
    
    // Deploy application
    const deployment = new k8s.apps.v1.Deployment("app", {
        metadata: {
            name: "my-app",
            namespace: "production",
        },
        spec: {
            replicas: 3,
            selector: { matchLabels: { app: "my-app" } },
            template: {
                metadata: { labels: { app: "my-app" } },
                spec: {
                    serviceAccountName: appServiceAccount.metadata.name,
                    automountServiceAccountToken: true,  // Only if app needs K8s API access
                    containers: [{
                        name: "app",
                        image: "myapp:v1.0.0",
                    }],
                },
            },
        },
    });
    

    Best Practices:

    • Create a dedicated service account for each application or microservice
    • Use descriptive names (e.g., app-name-sa)
    • Set automountServiceAccountToken: false by default; only enable when needed
    • Grant minimal RBAC permissions using Roles/RoleBindings
    • Consider Azure Workload Identity for accessing Azure resources
    • Never use the default service account for production workloads
    • Document the purpose of each service account in annotations
    • Regularly audit service account usage and permissions

    aks-defender-container-scanning-enabled

    Severity: medium · Enforcement: advisory

    Ensure container registries have vulnerability scanning enabled through Microsoft Defender for Cloud or third-party solutions.

    • 5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).
    Remediation
    Fix: Enable Vulnerability Scanning for Container Registry
    Step 1: Upgrade Registry SKU (if using Basic)
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "myregistry",
        sku: {
            name: "Standard",  // Or "Premium" for advanced features
        },
        dataEndpointEnabled: true,  // Enable for better performance
        // ... other config
    });
    
    Step 2: Enable Microsoft Defender for Container Registries
    import * as azure from "@pulumi/azure-native";
    
    // Enable Defender for Container Registries at the subscription level
    const defenderPlan = new azure.security.Pricing("containerRegistry", {
        pricingName: "ContainerRegistry",
        pricingTier: "Standard",
    });
    
    // The registry will automatically integrate with Defender for vulnerability scanning
    // Ensure the registry SKU is Standard or Premium (Basic does not support scanning)
    
    Step 3: Enable Quarantine Policy (Premium SKU)
    const registry = new azure.containerregistry.Registry("registry", {
        resourceGroupName: resourceGroup.name,
        registryName: "myregistry",
        sku: {
            name: "Premium",
        },
        policies: {
            quarantinePolicy: {
                status: "enabled",  // Quarantine unscanned images
            },
            trustPolicy: {
                status: "enabled",  // Require signed images (optional)
                type: "Notary",
            },
            retentionPolicy: {
                status: "enabled",
                days: 30,  // Retain images for 30 days
            },
        },
        dataEndpointEnabled: true,
    });
    
    Step 4: Integrate with Third-Party Scanner (Optional)
    // Example: Configure webhook for third-party scanning
    const webhook = new azure.containerregistry.Webhook("scanner-webhook", {
        resourceGroupName: resourceGroup.name,
        registryName: registry.name,
        webhookName: "scanner",
        location: resourceGroup.location,
        serviceUri: "https://scanner.example.com/webhook",
        actions: ["push"],
        status: "enabled",
        scope: "myapp/*:*",  // Scan all tags in myapp repositories
    });
    
    Step 5: Set Up Continuous Scanning
    // Task to scan all images in the registry
    const scanTask = new azure.containerregistry.Task("scan-task", {
        resourceGroupName: resourceGroup.name,
        registryName: registry.name,
        taskName: "scan-images",
        location: resourceGroup.location,
        platform: {
            os: "Linux",
        },
        step: {
            type: "Docker",
            dockerFilePath: "Dockerfile",
            contextPath: "https://github.com/Azure/acr-cli.git",
            imageNames: ["scan:{{.Run.ID}}"],
        },
        trigger: {
            timerTriggers: [{
                name: "daily-scan",
                schedule: "0 2 * * *",  // Run daily at 2 AM
            }],
        },
    });
    

    Best Practices:

    • Enable Microsoft Defender for all production registries
    • Use Premium SKU for production workloads requiring quarantine and trust policies
    • Implement CI/CD pipeline integration for automatic scanning on push
    • Set up notifications for critical vulnerabilities
    • Regularly review and act on vulnerability scan results
    • Consider content trust (image signing) for production images

    aks-load-balancer-tls-encryption

    Severity: high · Enforcement: advisory

    Ensure load balancers use TLS encryption for HTTPS traffic with proper certificate configuration.

    • 5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.
    Remediation
    Fix: Enable TLS Encryption for LoadBalancer Services
    import * as k8s from "@pulumi/kubernetes";
    
    const service = new k8s.core.v1.Service("my-service", {
        metadata: {
            name: "my-service",
            annotations: {
                // Azure Load Balancer with SSL certificate
                "service.beta.kubernetes.io/azure-load-balancer-ssl-cert": "my-ssl-cert",
                "service.beta.kubernetes.io/azure-load-balancer-ssl-ports": "443",
                // Optional: Use internal load balancer
                "service.beta.kubernetes.io/azure-load-balancer-internal": "true",
            },
        },
        spec: {
            type: "LoadBalancer",
            ports: [{
                name: "https",
                port: 443,
                targetPort: 8080,
                protocol: "TCP",
            }],
            selector: {
                app: "my-app",
            },
        },
    });
    

    aks-network-policy-enabled

    Severity: medium · Enforcement: advisory

    Ensure AKS clusters have network policy enabled to control traffic flow between pods, namespaces, and external networks.

    • 5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.
    Remediation
    Fix: Enable Network Policy for AKS Cluster
    Option 1: Azure CNI with Azure Network Policy
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "azure",
            networkPolicy: "azure",  // Azure network policy
            serviceCidr: "10.0.0.0/16",
            dnsServiceIP: "10.0.0.10",
        },
        // ... other config
    });
    
    Option 2: Azure CNI with Calico Network Policy
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "azure",
            networkPolicy: "calico",  // Calico network policy
            serviceCidr: "10.0.0.0/16",
            dnsServiceIP: "10.0.0.10",
        },
        // ... other config
    });
    
    Option 3: Kubenet with Calico Network Policy
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "kubenet",
            networkPolicy: "calico",  // Only Calico works with kubenet
            podCidr: "10.244.0.0/16",
            serviceCidr: "10.0.0.0/16",
            dnsServiceIP: "10.0.0.10",
        },
        // ... other config
    });
    

    Note: Azure network policy requires Azure CNI network plugin. Kubenet only supports Calico network policy.

    aks-private-endpoint-enabled

    Severity: high · Enforcement: advisory

    Ensure AKS clusters are created with private endpoint enabled and public access disabled.

    • 5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.
    Remediation
    Fix: Enable Private Endpoint for AKS Cluster

    Note: This policy mandates that clusters must use private endpoints. For detailed private cluster configuration validation, see the aks-cluster-endpoint-restrict-public-access policy.

    Option 1: Private Cluster with System-Managed DNS
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: true,
            enablePrivateClusterPublicFQDN: false,
            privateDNSZone: "system", // Azure manages the private DNS zone
        },
        // ... other config
    });
    
    Option 2: Private Cluster with Custom Private DNS Zone
    const privateDnsZone = new azure.network.PrivateDnsZone("aksPrivateDns", {
        resourceGroupName: resourceGroup.name,
        location: "global",
        privateZoneName: "privatelink.eastus.azmk8s.io", // Region-specific
    });
    
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: true,
            enablePrivateClusterPublicFQDN: false,
            privateDNSZone: privateDnsZone.id, // Custom private DNS zone
        },
        // ... other config
    });
    
    Option 3: Private Cluster with No Private DNS Zone (BYO DNS)
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        apiServerAccessProfile: {
            enablePrivateCluster: true,
            enablePrivateClusterPublicFQDN: false,
            privateDNSZone: "none", // Manage DNS resolution yourself
        },
        // ... other config
    });
    

    Important Notes:

    • Private clusters require network connectivity from where kubectl is run
    • Configure VPN or ExpressRoute for accessing private clusters
    • Private DNS zones must be linked to the virtual network
    • Consider using Azure Bastion or jump boxes for cluster management

    aks-private-nodes-enabled

    Severity: high · Enforcement: advisory

    Ensure AKS cluster nodes are private without public IP addresses assigned.

    • 5.4 Network Security — Restrict Access to the Control Plane Endpoint. Ensure clusters are created with Private Endpoints and Private Nodes. Enable Network Policy. Encrypt traffic to HTTPS load balancers.
    Remediation
    Fix: Disable Public IPs for AKS Cluster Nodes
    Option 1: Private Nodes with Load Balancer Outbound (Default)
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "azure",
            outboundType: "loadBalancer", // NAT through Azure Load Balancer
            loadBalancerProfile: {
                managedOutboundIPs: {
                    count: 1, // Number of managed public IPs for outbound
                },
            },
        },
        agentPoolProfiles: [{
            name: "agentpool",
            count: 3,
            vmSize: "Standard_DS2_v2",
            enableNodePublicIP: false, // No public IPs on nodes
            vnetSubnetID: subnet.id,
            mode: "System",
        }],
        // ... other config
    });
    
    Option 2: Private Nodes with User-Defined Routing (Most Secure)
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "azure",
            outboundType: "userDefinedRouting", // Route through firewall/NVA
            serviceCidr: "10.0.0.0/16",
            dnsServiceIP: "10.0.0.10",
        },
        agentPoolProfiles: [{
            name: "agentpool",
            count: 3,
            vmSize: "Standard_DS2_v2",
            enableNodePublicIP: false, // No public IPs on nodes
            vnetSubnetID: subnet.id, // Subnet must have route table configured
            mode: "System",
        }],
        // ... other config
    });
    
    Option 3: Private Nodes with NAT Gateway
    const natGateway = new azure.network.NatGateway("natGateway", {
        resourceGroupName: resourceGroup.name,
        location: resourceGroup.location,
        sku: {
            name: "Standard",
        },
    });
    
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        networkProfile: {
            networkPlugin: "azure",
            outboundType: "managedNATGateway", // Or "userAssignedNATGateway"
            natGatewayProfile: {
                managedOutboundIPProfile: {
                    count: 1,
                },
            },
        },
        agentPoolProfiles: [{
            name: "agentpool",
            count: 3,
            vmSize: "Standard_DS2_v2",
            enableNodePublicIP: false, // No public IPs on nodes
            vnetSubnetID: subnet.id,
            mode: "System",
        }],
        // ... other config
    });
    

    Important Notes:

    • User-defined routing requires proper route table configuration on the subnet
    • Ensure egress firewall rules are configured when using user-defined routing
    • NAT Gateway provides better SNAT port allocation for high-scale scenarios

    aks-secrets-encryption-enabled

    Severity: critical · Enforcement: advisory

    Ensure AKS clusters have encryption at rest enabled for Kubernetes secrets stored in etcd.

    • 5.3 Encryption — Ensure Kubernetes Secrets are encrypted.
    Remediation
    Fix: Enable Secrets Encryption for AKS Cluster
    Option 1: Enable Encryption at Host (Platform-Managed Keys)
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        agentPoolProfiles: [{
            name: "agentpool",
            count: 3,
            vmSize: "Standard_DS2_v2",
            enableEncryptionAtHost: true,  // Enable encryption at host
            osDiskSizeGB: 128,
            mode: "System",
        }],
        // ... other config
    });
    
    Option 2: Use Customer-Managed Keys with Azure Key Vault
    const keyVault = new azure.keyvault.Vault("vault", {
        resourceGroupName: resourceGroup.name,
        location: resourceGroup.location,
        tenantId: tenantId,
        sku: {
            family: "A",
            name: "standard",
        },
        enabledForDiskEncryption: true,
    });
    
    const key = new azure.keyvault.Key("key", {
        keyVaultId: keyVault.id,
        keyType: "RSA",
        keySize: 2048,
        keyOpts: ["encrypt", "decrypt", "wrapKey", "unwrapKey"],
    });
    
    const diskEncryptionSet = new azure.compute.DiskEncryptionSet("des", {
        resourceGroupName: resourceGroup.name,
        location: resourceGroup.location,
        identity: {
            type: "SystemAssigned",
        },
        encryptionType: "EncryptionAtRestWithCustomerKey",
        activeKey: {
            keyUrl: key.keyUriWithVersion,
            sourceVault: {
                id: keyVault.id,
            },
        },
    });
    
    const cluster = new azure.containerservice.ManagedCluster("cluster", {
        resourceGroupName: resourceGroup.name,
        diskEncryptionSetID: diskEncryptionSet.id,  // Use customer-managed keys
        agentPoolProfiles: [{
            name: "agentpool",
            count: 3,
            vmSize: "Standard_DS2_v2",
            enableEncryptionAtHost: true,
            osDiskSizeGB: 128,
            mode: "System",
        }],
        // ... other config
    });
    

    Note: Encryption at host must be enabled on your Azure subscription before use.

    k8s-cluster-admin-role-binding-minimized

    Severity: high · Enforcement: advisory

    Ensure cluster-admin ClusterRole is bound to a minimal number of users and service accounts.

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Minimize cluster-admin Role Bindings

    Use more specific roles instead of cluster-admin:

    // Instead of binding cluster-admin, create specific roles
    const role = new k8s.rbac.v1.Role("specificRole", {
        metadata: { namespace: "default" },
        rules: [{
            apiGroups: [""],
            resources: ["pods"],
            verbs: ["get", "list", "watch"],
        }],
    });
    
    const roleBinding = new k8s.rbac.v1.RoleBinding("specificBinding", {
        metadata: { namespace: "default" },
        roleRef: {
            apiGroup: "rbac.authorization.k8s.io",
            kind: "Role",
            name: role.metadata.name,
        },
        subjects: [{
            kind: "User",
            name: "specific-user",
        }],
    });
    

    k8s-default-namespace-not-used

    Severity: medium · Enforcement: advisory

    Ensure workloads are not deployed to the default namespace.

    • 4.5-4.6 Secret Management and Namespaces — Prefer using secrets as files over secrets as environment variables. Ensure default namespace is not used for workloads. Apply security context to pods and containers.
    Remediation
    Fix: Use Dedicated Namespaces

    Create and use dedicated namespaces:

    const namespace = new k8s.core.v1.Namespace("app-namespace", {
        metadata: {
            name: "my-app",
        },
    });
    
    const deployment = new k8s.apps.v1.Deployment("app", {
        metadata: {
            namespace: namespace.metadata.name,
        },
        // ... rest of deployment spec
    });
    

    k8s-default-service-accounts-not-used

    Severity: medium · Enforcement: advisory

    Ensure workloads do not use the default service account.

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Use Dedicated Service Accounts

    Create and use dedicated service accounts for workloads:

    const serviceAccount = new k8s.core.v1.ServiceAccount("appServiceAccount", {
        metadata: {
            namespace: "default",
            name: "my-app-sa",
        },
    });
    
    const deployment = new k8s.apps.v1.Deployment("app", {
        spec: {
            template: {
                spec: {
                    serviceAccountName: serviceAccount.metadata.name,
                    // ... rest of pod spec
                },
            },
        },
    });
    

    k8s-namespaces-network-policies-defined

    Severity: medium · Enforcement: advisory

    Ensure all namespaces have network policies defined to control pod-to-pod communication and implement network segmentation.

    • 4.4 Network Policies — Ensure that all Namespaces have Network Policies defined.
    Remediation
    Fix: Define Network Policies for All Namespaces
    Step 1: Create Default Deny Network Policies
    import * as k8s from "@pulumi/kubernetes";
    
    // Create namespace with label
    const namespace = new k8s.core.v1.Namespace("app-namespace", {
        metadata: {
            name: "production",
            labels: {
                name: "production",
                environment: "prod",
            },
        },
    });
    
    // Default deny all ingress
    const denyAllIngress = new k8s.networking.v1.NetworkPolicy("deny-all-ingress", {
        metadata: {
            name: "default-deny-ingress",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {},  // Applies to all pods
            policyTypes: ["Ingress"],
        },
    });
    
    // Default deny all egress
    const denyAllEgress = new k8s.networking.v1.NetworkPolicy("deny-all-egress", {
        metadata: {
            name: "default-deny-egress",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {},  // Applies to all pods
            policyTypes: ["Egress"],
        },
    });
    
    // Allow specific ingress traffic
    const allowIngress = new k8s.networking.v1.NetworkPolicy("allow-ingress", {
        metadata: {
            name: "allow-from-frontend",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {
                matchLabels: { app: "api" },
            },
            policyTypes: ["Ingress"],
            ingress: [{
                from: [
                    {
                        namespaceSelector: {
                            matchLabels: { name: "frontend" },
                        },
                    },
                    {
                        podSelector: {
                            matchLabels: { app: "web" },
                        },
                    },
                ],
                ports: [{
                    protocol: "TCP",
                    port: 8080,
                }],
            }],
        },
    });
    
    // Allow DNS and specific egress
    const allowEgress = new k8s.networking.v1.NetworkPolicy("allow-egress", {
        metadata: {
            name: "allow-to-services",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {
                matchLabels: { app: "api" },
            },
            policyTypes: ["Egress"],
            egress: [
                // Allow database access
                {
                    to: [{
                        namespaceSelector: {
                            matchLabels: { name: "database" },
                        },
                        podSelector: {
                            matchLabels: { app: "postgres" },
                        },
                    }],
                    ports: [{
                        protocol: "TCP",
                        port: 5432,
                    }],
                },
                // Allow DNS
                {
                    to: [{
                        namespaceSelector: {
                            matchLabels: { name: "kube-system" },
                        },
                        podSelector: {
                            matchLabels: { "k8s-app": "kube-dns" },
                        },
                    }],
                    ports: [{
                        protocol: "UDP",
                        port: 53,
                    }],
                },
            ],
        },
    });
    
    Step 2: Additional Network Policy Patterns
    // Allow traffic within same namespace only
    const allowSameNamespace = new k8s.networking.v1.NetworkPolicy("allow-same-namespace", {
        metadata: {
            name: "allow-same-namespace",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {},  // Applies to all pods
            policyTypes: ["Ingress"],
            ingress: [{
                from: [{
                    podSelector: {},  // All pods in same namespace
                }],
            }],
        },
    });
    
    // Allow traffic from ingress controller
    const allowFromIngress = new k8s.networking.v1.NetworkPolicy("allow-from-ingress", {
        metadata: {
            name: "allow-from-ingress",
            namespace: namespace.metadata.name,
        },
        spec: {
            podSelector: {
                matchLabels: { expose: "true" },
            },
            policyTypes: ["Ingress"],
            ingress: [{
                from: [
                    {
                        namespaceSelector: {
                            matchLabels: { name: "ingress-nginx" },
                        },
                    },
                    {
                        podSelector: {
                            matchLabels: { "app.kubernetes.io/name": "ingress-nginx" },
                        },
                    },
                ],
            }],
        },
    });
    

    Best Practices:

    • Start with default-deny policies (deny all ingress/egress)
    • Explicitly allow only required traffic flows
    • Use namespace and pod selectors for fine-grained control
    • Always allow DNS (UDP 53 to kube-dns)
    • Document the purpose of each network policy
    • Test network policies in non-production first
    • Use tools like kubectl auth can-i to verify connectivity
    • Consider using Calico or Cilium for enhanced network policy features

    k8s-pod-security-allow-privilege-escalation-minimized

    Severity: high · Enforcement: advisory

    Ensure containers do not allow privilege escalation.

    • 4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
    Remediation
    Fix: Disable Privilege Escalation

    Set allowPrivilegeEscalation to false in container security context:

    const pod = new k8s.core.v1.Pod("pod", {
        spec: {
            containers: [{
                name: "app",
                image: "nginx",
                securityContext: {
                    allowPrivilegeEscalation: false,
                    runAsNonRoot: true,
                    capabilities: {
                        drop: ["ALL"],
                    },
                },
            }],
        },
    });
    

    k8s-pod-security-context-applied

    Severity: medium · Enforcement: advisory

    Ensure security context is applied to pods and containers with appropriate security settings for defense in depth.

    • 4.5-4.6 Secret Management and Namespaces — Prefer using secrets as files over secrets as environment variables. Ensure default namespace is not used for workloads. Apply security context to pods and containers.
    Remediation
    Fix: Apply Security Context to Pods and Containers
    Step 1: Create Deployment with Basic Security Context
    import * as k8s from "@pulumi/kubernetes";
    
    const deployment = new k8s.apps.v1.Deployment("secure-app", {
        metadata: {
            name: "secure-app",
            namespace: "production",
        },
        spec: {
            replicas: 3,
            selector: {
                matchLabels: { app: "secure-app" },
            },
            template: {
                metadata: {
                    labels: { app: "secure-app" },
                },
                spec: {
                    // Pod-level security context
                    securityContext: {
                        runAsNonRoot: true,
                        runAsUser: 1000,
                        runAsGroup: 3000,
                        fsGroup: 2000,
                        seccompProfile: {
                            type: "RuntimeDefault",
                        },
                    },
                    containers: [{
                        name: "app",
                        image: "myapp:v1.0.0",
                        // Container-level security context
                        securityContext: {
                            allowPrivilegeEscalation: false,
                            readOnlyRootFilesystem: true,
                            runAsNonRoot: true,
                            runAsUser: 1000,
                            capabilities: {
                                drop: ["ALL"],
                            },
                            seccompProfile: {
                                type: "RuntimeDefault",
                            },
                        },
                        volumeMounts: [
                            {
                                name: "tmp",
                                mountPath: "/tmp",
                            },
                            {
                                name: "cache",
                                mountPath: "/app/cache",
                            },
                        ],
                        resources: {
                            requests: {
                                cpu: "100m",
                                memory: "128Mi",
                            },
                            limits: {
                                cpu: "500m",
                                memory: "512Mi",
                            },
                        },
                    }],
                    volumes: [
                        {
                            name: "tmp",
                            emptyDir: { sizeLimit: "100Mi" },
                        },
                        {
                            name: "cache",
                            emptyDir: { sizeLimit: "1Gi" },
                        },
                    ],
                },
            },
        },
    });
    
    Step 2: Hardened Deployment with Read-Only Root Filesystem
    const hardenedDeployment = new k8s.apps.v1.Deployment("hardened-app", {
        metadata: {
            name: "hardened-app",
            namespace: "production",
        },
        spec: {
            replicas: 3,
            selector: {
                matchLabels: { app: "hardened-app" },
            },
            template: {
                metadata: {
                    labels: { app: "hardened-app" },
                },
                spec: {
                    securityContext: {
                        runAsNonRoot: true,
                        runAsUser: 10001,  // High UID (non-system user)
                        fsGroup: 10001,
                        seccompProfile: {
                            type: "RuntimeDefault",
                        },
                    },
                    containers: [{
                        name: "app",
                        image: "myapp:v1.0.0",
                        securityContext: {
                            allowPrivilegeEscalation: false,
                            privileged: false,
                            readOnlyRootFilesystem: true,
                            runAsNonRoot: true,
                            capabilities: {
                                drop: ["ALL"],
                                // Only add if absolutely necessary
                                // add: ["NET_BIND_SERVICE"],  // For binding to ports < 1024
                            },
                            seccompProfile: {
                                type: "RuntimeDefault",
                            },
                        },
                        volumeMounts: [
                            {
                                name: "tmp",
                                mountPath: "/tmp",
                            },
                            {
                                name: "app-data",
                                mountPath: "/app/data",
                            },
                        ],
                    }],
                    volumes: [
                        {
                            name: "tmp",
                            emptyDir: { sizeLimit: "100Mi" },
                        },
                        {
                            name: "app-data",
                            persistentVolumeClaim: {
                                claimName: "app-data-pvc",
                            },
                        },
                    ],
                },
            },
        },
    });
    
    Step 3: Enforce Pod Security Standards at Namespace Level
    // Create namespace with Pod Security Standards labels
    const productionNamespace = new k8s.core.v1.Namespace("production", {
        metadata: {
            name: "production",
            labels: {
                // Restricted is the most secure PSS level
                "pod-security.kubernetes.io/enforce": "restricted",
                "pod-security.kubernetes.io/enforce-version": "latest",
                "pod-security.kubernetes.io/audit": "restricted",
                "pod-security.kubernetes.io/warn": "restricted",
            },
        },
    });
    

    Security Context Fields Reference:

    Pod-Level Security Context:

    • runAsUser: UID to run pod processes (avoid 0/root)
    • runAsGroup: GID to run pod processes
    • runAsNonRoot: Enforce non-root user
    • fsGroup: Group for volume ownership
    • supplementalGroups: Additional groups
    • seccompProfile: Seccomp profile (RuntimeDefault recommended)
    • seLinuxOptions: SELinux context
    • fsGroupChangePolicy: How fsGroup is applied

    Container-Level Security Context:

    • runAsUser: UID for container (overrides pod)
    • runAsGroup: GID for container (overrides pod)
    • runAsNonRoot: Enforce non-root (overrides pod)
    • readOnlyRootFilesystem: Immutable root FS
    • allowPrivilegeEscalation: Prevent privilege escalation
    • privileged: Run as privileged (never use)
    • capabilities: Add/drop Linux capabilities
    • seccompProfile: Seccomp profile
    • seLinuxOptions: SELinux context

    Best Practices:

    • Always set runAsNonRoot: true
    • Use high UIDs (>= 1000) to avoid conflicts
    • Drop all capabilities, add only required ones
    • Use readOnlyRootFilesystem: true with volume mounts for writable dirs
    • Set allowPrivilegeEscalation: false
    • Use seccompProfile: RuntimeDefault for syscall filtering
    • Apply Pod Security Standards at namespace level
    • Test security context settings in non-prod first

    k8s-pod-security-host-ipc-minimized

    Severity: high · Enforcement: advisory

    Ensure containers do not share the host IPC namespace.

    • 4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
    Remediation
    Fix: Disable Host IPC Sharing

    Remove hostIPC from pod spec:

    const pod = new k8s.core.v1.Pod("pod", {
        spec: {
            hostIPC: false,  // Explicitly set to false or remove
            containers: [{
                name: "app",
                image: "nginx",
            }],
        },
    });
    

    k8s-pod-security-host-network-minimized

    Severity: high · Enforcement: advisory

    Ensure containers do not share the host network namespace.

    • 4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
    Remediation
    Fix: Disable Host Network Sharing

    Remove hostNetwork from pod spec:

    const pod = new k8s.core.v1.Pod("pod", {
        spec: {
            hostNetwork: false,  // Explicitly set to false or remove
            containers: [{
                name: "app",
                image: "nginx",
            }],
        },
    });
    

    k8s-pod-security-host-pid-minimized

    Severity: high · Enforcement: advisory

    Ensure containers do not share the host process ID namespace.

    • 4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
    Remediation
    Fix: Disable Host PID Sharing

    Remove hostPID from pod spec:

    const pod = new k8s.core.v1.Pod("pod", {
        spec: {
            hostPID: false,  // Explicitly set to false or remove
            containers: [{
                name: "app",
                image: "nginx",
            }],
        },
    });
    

    k8s-pod-security-privileged-containers-minimized

    Severity: critical · Enforcement: advisory

    Ensure containers do not run in privileged mode.

    • 4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
    Remediation
    Fix: Disable Privileged Mode

    Remove privileged flag from container security context:

    const pod = new k8s.core.v1.Pod("pod", {
        spec: {
            containers: [{
                name: "app",
                image: "nginx",
                securityContext: {
                    privileged: false,  // Explicitly set to false or remove
                    runAsNonRoot: true,
                    capabilities: {
                        drop: ["ALL"],
                    },
                },
            }],
        },
    });
    

    k8s-rbac-create-pods-minimized

    Severity: medium · Enforcement: advisory

    Ensure Roles and ClusterRoles do not grant excessive permissions to create pods or resources that can create pods (deployments, jobs, etc.).

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Minimize Pod Creation Permissions

    Avoid granting create permissions on pods and pod-creating resources:

    // Good: Read-only access to pods
    const podReader = new k8s.rbac.v1.Role("podReader", {
        metadata: { namespace: "default" },
        rules: [{
            apiGroups: [""],
            resources: ["pods"],
            verbs: ["get", "list", "watch"],  // Read-only
        }],
    });
    
    // Bad: Avoid granting pod creation abilities
    const podCreator = new k8s.rbac.v1.Role("podCreator", {
        rules: [{
            apiGroups: [""],
            resources: ["pods", "pods/exec", "pods/attach"],
            verbs: ["create", "*"],  // DON'T DO THIS unless absolutely necessary
        }],
    });
    

    Resources that can create pods and should be restricted:

    • pods, pods/exec, pods/attach, pods/portforward
    • deployments, replicasets, statefulsets, daemonsets, jobs, cronjobs
    • replicationcontrollers

    k8s-rbac-secret-access-minimized

    Severity: critical · Enforcement: advisory

    Ensure Roles and ClusterRoles do not grant excessive permissions to secrets (create, update, patch, delete, or wildcard verbs).

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Minimize Secret Access Permissions

    Limit secret access to read-only (get, list, watch) unless write access is explicitly required:

    // Good: Read-only secret access
    const readOnlyRole = new k8s.rbac.v1.Role("secretReader", {
        metadata: { namespace: "default" },
        rules: [{
            apiGroups: [""],
            resources: ["secrets"],
            verbs: ["get", "list", "watch"],  // Read-only access
        }],
    });
    
    // Bad: Avoid granting write access to secrets
    const writeRole = new k8s.rbac.v1.Role("secretWriter", {
        rules: [{
            apiGroups: [""],
            resources: ["secrets"],
            verbs: ["*"],  // DON'T DO THIS
        }],
    });
    

    Only grant create/update/delete/patch permissions when absolutely necessary and document the justification.

    k8s-rbac-wildcard-use-minimized

    Severity: high · Enforcement: advisory

    Ensure Roles and ClusterRoles do not use wildcards for resources, verbs, or apiGroups.

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Remove Wildcards from RBAC Rules

    Be specific about resources, verbs, and API groups:

    const role = new k8s.rbac.v1.Role("specificRole", {
        metadata: { namespace: "default" },
        rules: [{
            apiGroups: [""],  // Specific API group, not "*"
            resources: ["pods", "services"],  // Specific resources, not "*"
            verbs: ["get", "list", "watch"],  // Specific verbs, not "*"
        }],
    });
    

    k8s-resource-namespace-boundaries

    Severity: low · Enforcement: advisory

    Ensure administrative boundaries are maintained between resources using namespaces for proper multi-tenancy and resource isolation.

    • 4.5-4.6 Secret Management and Namespaces — Prefer using secrets as files over secrets as environment variables. Ensure default namespace is not used for workloads. Apply security context to pods and containers.
    Remediation
    Fix: Establish Proper Namespace Boundaries
    Step 1: Create Namespaces with Resource Quotas and Limits
    import * as k8s from "@pulumi/kubernetes";
    
    // Create production namespace
    const prodNamespace = new k8s.core.v1.Namespace("production", {
        metadata: {
            name: "production",
            labels: {
                environment: "production",
                team: "platform",
                "cost-center": "engineering",
            },
            annotations: {
                description: "Production environment workloads",
            },
        },
    });
    
    // Create resource quota
    const prodQuota = new k8s.core.v1.ResourceQuota("prod-quota", {
        metadata: {
            name: "compute-quota",
            namespace: prodNamespace.metadata.name,
        },
        spec: {
            hard: {
                "requests.cpu": "100",
                "requests.memory": "200Gi",
                "limits.cpu": "200",
                "limits.memory": "400Gi",
                "persistentvolumeclaims": "10",
                "pods": "50",
            },
        },
    });
    
    // Create limit range
    const prodLimits = new k8s.core.v1.LimitRange("prod-limits", {
        metadata: {
            name: "resource-limits",
            namespace: prodNamespace.metadata.name,
        },
        spec: {
            limits: [
                {
                    type: "Container",
                    default: {
                        cpu: "500m",
                        memory: "512Mi",
                    },
                    defaultRequest: {
                        cpu: "100m",
                        memory: "128Mi",
                    },
                    max: {
                        cpu: "2",
                        memory: "4Gi",
                    },
                    min: {
                        cpu: "50m",
                        memory: "64Mi",
                    },
                },
                {
                    type: "Pod",
                    max: {
                        cpu: "4",
                        memory: "8Gi",
                    },
                },
            ],
        },
    });
    
    // Deploy application in proper namespace
    const deployment = new k8s.apps.v1.Deployment("app", {
        metadata: {
            name: "my-app",
            namespace: prodNamespace.metadata.name,  // Use dedicated namespace
            labels: {
                app: "my-app",
                environment: "production",
            },
        },
        spec: {
            replicas: 3,
            selector: {
                matchLabels: { app: "my-app" },
            },
            template: {
                metadata: {
                    labels: { app: "my-app" },
                },
                spec: {
                    containers: [{
                        name: "app",
                        image: "myapp:v1.0.0",
                        resources: {
                            requests: {
                                cpu: "100m",
                                memory: "128Mi",
                            },
                            limits: {
                                cpu: "500m",
                                memory: "512Mi",
                            },
                        },
                    }],
                },
            },
        },
    });
    
    Step 2: Configure RBAC for Namespace Isolation
    // Create development namespace
    const devNamespace = new k8s.core.v1.Namespace("development", {
        metadata: {
            name: "development",
            labels: {
                environment: "development",
                team: "platform",
            },
        },
    });
    
    // Create role for namespace-scoped access
    const developerRole = new k8s.rbac.v1.Role("developer", {
        metadata: {
            name: "developer",
            namespace: devNamespace.metadata.name,
        },
        rules: [{
            apiGroups: ["", "apps", "batch"],
            resources: ["pods", "deployments", "jobs", "configmaps", "secrets"],
            verbs: ["get", "list", "watch", "create", "update", "patch", "delete"],
        }],
    });
    
    // Bind role to team
    const developersBinding = new k8s.rbac.v1.RoleBinding("developers", {
        metadata: {
            name: "developers",
            namespace: devNamespace.metadata.name,
        },
        subjects: [{
            kind: "Group",
            name: "developers",
            apiGroup: "rbac.authorization.k8s.io",
        }],
        roleRef: {
            kind: "Role",
            name: developerRole.metadata.name,
            apiGroup: "rbac.authorization.k8s.io",
        },
    });
    

    Best Practices:

    • Create separate namespaces for environments (prod, staging, dev)
    • Use namespaces for team or application boundaries
    • Set resource quotas to prevent resource exhaustion
    • Define limit ranges for default resource constraints
    • Use RBAC to restrict access to specific namespaces
    • Label namespaces consistently (environment, team, cost-center)
    • Never deploy production workloads in default namespace
    • Consider using namespace-as-a-service tools (Hierarchical Namespace Controller)
    • Monitor namespace resource usage and quota limits

    k8s-secrets-as-files-not-env-vars

    Severity: medium · Enforcement: advisory

    Ensure secrets are mounted as files via volumes rather than exposed as environment variables.

    • 4.5-4.6 Secret Management and Namespaces — Prefer using secrets as files over secrets as environment variables. Ensure default namespace is not used for workloads. Apply security context to pods and containers.
    Remediation
    Fix: Mount Secrets as Files

    Use volume mounts instead of environment variables for secrets:

    // Bad: Using secrets as environment variables
    const badPod = new k8s.core.v1.Pod("bad-pod", {
        spec: {
            containers: [{
                name: "app",
                image: "myapp:latest",
                env: [{
                    name: "DB_PASSWORD",
                    valueFrom: {
                        secretKeyRef: {
                            name: "db-secret",
                            key: "password",
                        },
                    },
                }],
            }],
        },
    });
    
    // Good: Mount secrets as files
    const goodPod = new k8s.core.v1.Pod("good-pod", {
        spec: {
            containers: [{
                name: "app",
                image: "myapp:latest",
                volumeMounts: [{
                    name: "secret-volume",
                    mountPath: "/etc/secrets",
                    readOnly: true,
                }],
            }],
            volumes: [{
                name: "secret-volume",
                secret: {
                    secretName: "db-secret",
                },
            }],
        },
    });
    // Access secret via: cat /etc/secrets/password
    

    Benefits of file-based secrets:

    • Not visible in process listings
    • Not logged by default
    • Can be rotated without container restart (with proper file watching)

    k8s-service-account-token-mounted-minimized

    Severity: medium · Enforcement: advisory

    Ensure service account tokens are not automatically mounted in pods unless explicitly required.

    • 4.1 RBAC and Authentication — Restrict administrator privileges to dedicated administrator accounts on enterprise assets. Implement proper RBAC controls.
    Remediation
    Fix: Disable Service Account Token Auto-Mounting

    Set automountServiceAccountToken: false unless the pod needs Kubernetes API access:

    // Good: Disable auto-mounting when not needed
    const pod = new k8s.core.v1.Pod("myPod", {
        metadata: { name: "my-pod" },
        spec: {
            automountServiceAccountToken: false,  // Explicitly disable
            containers: [{
                name: "app",
                image: "nginx:latest",
            }],
        },
    });
    
    // Also disable at ServiceAccount level
    const sa = new k8s.core.v1.ServiceAccount("myServiceAccount", {
        metadata: { name: "my-sa" },
        automountServiceAccountToken: false,  // Disable by default
    });
    

    Only enable when the pod explicitly requires Kubernetes API access.

      The infrastructure as code platform for any cloud.