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.
- k8s-cluster-admin-role-binding-minimized
- k8s-default-service-accounts-not-used
- k8s-rbac-create-pods-minimized
- k8s-rbac-secret-access-minimized
- k8s-rbac-wildcard-use-minimized
- k8s-service-account-token-mounted-minimized
4.2 Pod Security — Minimize the admission of privileged containers and containers with dangerous capabilities.
- k8s-pod-security-allow-privilege-escalation-minimized
- k8s-pod-security-host-ipc-minimized
- k8s-pod-security-host-network-minimized
- k8s-pod-security-host-pid-minimized
- k8s-pod-security-privileged-containers-minimized
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.
- k8s-default-namespace-not-used
- k8s-pod-security-context-applied
- k8s-resource-namespace-boundaries
- k8s-secrets-as-files-not-env-vars
5.1 Container Registry Security — Ensure Image Vulnerability Scanning using Microsoft Defender for Cloud (MDC). Minimize user access to Azure Container Registry (ACR).
- acr-user-access-minimized
- aks-acr-readonly-access
- aks-approved-registries-only
- aks-defender-container-scanning-enabled
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.
- aks-cluster-endpoint-restrict-public-access
- aks-load-balancer-tls-encryption
- aks-network-policy-enabled
- aks-private-endpoint-enabled
- aks-private-nodes-enabled
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",
}],
},
},
},
});
Step 4: Use AKS ACR Integration (Recommended)
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
Option 1: Create a Private Cluster (Recommended)
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
Recommended: Use Azure Workload Identity (for AKS with Azure resource access)
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: falseby default; only enable when needed - Grant minimal RBAC permissions using Roles/RoleBindings
- Consider Azure Workload Identity for accessing Azure resources
- Never use the
defaultservice 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-ito 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 processesrunAsNonRoot: Enforce non-root userfsGroup: Group for volume ownershipsupplementalGroups: Additional groupsseccompProfile: Seccomp profile (RuntimeDefault recommended)seLinuxOptions: SELinux contextfsGroupChangePolicy: 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 FSallowPrivilegeEscalation: Prevent privilege escalationprivileged: Run as privileged (never use)capabilities: Add/drop Linux capabilitiesseccompProfile: Seccomp profileseLinuxOptions: SELinux context
Best Practices:
- Always set
runAsNonRoot: true - Use high UIDs (>= 1000) to avoid conflicts
- Drop all capabilities, add only required ones
- Use
readOnlyRootFilesystem: truewith volume mounts for writable dirs - Set
allowPrivilegeEscalation: false - Use
seccompProfile: RuntimeDefaultfor 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
defaultnamespace - 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.