diff --git a/quickstart/101-aks-entra-k8s-rbac/README.md b/quickstart/101-aks-entra-k8s-rbac/README.md new file mode 100644 index 000000000..a6da2a12d --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/README.md @@ -0,0 +1,121 @@ +# Use Microsoft Entra ID Groups with Kubernetes RBAC in AKS + +This template uses Microsoft Entra ID groups with Kubernetes role-based access control (Kubernetes RBAC) in an existing Azure Kubernetes Service (AKS) cluster. + +The example scopes two existing Microsoft Entra groups to namespaces: + +- The `appdev` group can manage resources in the `dev` namespace. +- The `opssre` group can manage resources in the `sre` namespace. + +The AKS cluster must already have Microsoft Entra integration and Kubernetes RBAC enabled. Azure RBAC for Kubernetes Authorization must be disabled for this example. + +## Prerequisites + +- An Azure subscription +- An existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled +- Azure RBAC for Kubernetes Authorization disabled on the cluster +- Terraform `>= 1.6.0` installed +- Azure CLI and `kubectl` installed +- Permission to assign Azure roles at the AKS cluster scope +- Permission to manage Kubernetes resources on the AKS cluster +- Two existing Microsoft Entra security groups +- Test users already assigned to those groups + +> **Note**: The `prequisite` folder contains the Terraform configuration used by this repository's end-to-end test. It creates a resource group and an AKS cluster with Microsoft Entra integration and Kubernetes RBAC, then supplies existing test principal IDs to the example. + +Sign in to Azure and select the subscription to use: + +```console +az login +az account set --subscription +``` + +## Terraform providers and variables + +This sample uses the AzureRM provider to reference the existing AKS cluster and assign Azure permissions, and the Kubernetes provider to create namespaces, Roles, and RoleBindings. + +The Terraform variables require values for the existing cluster and the object IDs of existing groups: + +```hcl +resource_group_name = "" +aks_cluster_name = "" +appdev_group_object_id = "" +opssre_group_object_id = "" +``` + +## Example + +Create a `terraform.tfvars` file with values for the existing cluster and group object IDs, then initialize, format, and validate the configuration: + +```console +terraform init +terraform fmt +terraform validate +``` + +Review and apply the configuration: + +```console +terraform plan +terraform apply +``` + +The configuration creates the following Azure and Kubernetes resources: + +- Cluster User Role assignments for both groups +- `dev` and `sre` Kubernetes namespaces +- Namespace-scoped Kubernetes Roles and RoleBindings + +## Verify namespace access + +Get credentials for the AKS cluster: + +```console +az aks get-credentials \ + --resource-group \ + --name +``` + +Verify that both namespaces exist: + +```console +kubectl get namespaces +``` + +The output should include `dev` and `sre`. + +## Test appdev access + +Authenticate as a user that is already a member of the appdev group and create a pod in the `dev` namespace: + +```console +kubectl run nginx-dev \ + --image=nginx \ + --restart=Never \ + --namespace dev +kubectl get pods --namespace dev +``` + +Listing pods across all namespaces or creating a pod in the `sre` namespace should return a `Forbidden` error because the appdev group is scoped to `dev`. + +## Test opssre access + +Authenticate as a user that is already a member of the opssre group and create a pod in the `sre` namespace: + +```console +kubectl run nginx-sre \ + --image=nginx \ + --restart=Never \ + --namespace sre +kubectl get pods --namespace sre +``` + +Creating a pod in the `dev` namespace should return a `Forbidden` error because the opssre group is scoped to `sre`. + +## Clean up + +Remove the namespaces, RoleBindings, Roles, and role assignments created by this configuration: + +```console +terraform destroy +``` diff --git a/quickstart/101-aks-entra-k8s-rbac/main.tf b/quickstart/101-aks-entra-k8s-rbac/main.tf new file mode 100644 index 000000000..6425ba3e3 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/main.tf @@ -0,0 +1,150 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + } +} + +provider "azurerm" { + features {} +} + +variable "resource_group_name" { + type = string + description = "Name of the resource group that contains the existing AKS cluster." +} + +variable "aks_cluster_name" { + type = string + description = "Name of the existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled." +} + +variable "appdev_group_object_id" { + type = string + description = "Object ID of an existing Microsoft Entra group used for developer access to the dev namespace." +} + +variable "opssre_group_object_id" { + type = string + description = "Object ID of an existing Microsoft Entra group used for SRE access to the sre namespace." +} + +data "azurerm_kubernetes_cluster" "aks" { + name = var.aks_cluster_name + resource_group_name = var.resource_group_name +} + +provider "kubernetes" { + host = data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].host + client_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_certificate) + client_key = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_key) + cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate) +} + +resource "azurerm_role_assignment" "appdev_cluster_user" { + scope = data.azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster User Role" + principal_id = var.appdev_group_object_id +} + +resource "azurerm_role_assignment" "opssre_cluster_user" { + scope = data.azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster User Role" + principal_id = var.opssre_group_object_id +} + +resource "kubernetes_namespace" "dev" { + metadata { + name = "dev" + } +} + +resource "kubernetes_namespace" "sre" { + metadata { + name = "sre" + } +} + +resource "kubernetes_role" "dev_full_access" { + metadata { + name = "dev-user-full-access" + namespace = kubernetes_namespace.dev.metadata[0].name + } + + rule { + api_groups = ["", "extensions", "apps"] + resources = ["*"] + verbs = ["*"] + } + + rule { + api_groups = ["batch"] + resources = ["jobs", "cronjobs"] + verbs = ["*"] + } +} + +resource "kubernetes_role" "sre_full_access" { + metadata { + name = "sre-user-full-access" + namespace = kubernetes_namespace.sre.metadata[0].name + } + + rule { + api_groups = ["", "extensions", "apps"] + resources = ["*"] + verbs = ["*"] + } + + rule { + api_groups = ["batch"] + resources = ["jobs", "cronjobs"] + verbs = ["*"] + } +} + +resource "kubernetes_role_binding" "dev_user_access" { + metadata { + name = "dev-user-access" + namespace = kubernetes_namespace.dev.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.dev_full_access.metadata[0].name + } + + subject { + kind = "Group" + name = var.appdev_group_object_id + api_group = "rbac.authorization.k8s.io" + } +} + +resource "kubernetes_role_binding" "sre_user_access" { + metadata { + name = "sre-user-access" + namespace = kubernetes_namespace.sre.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.sre_full_access.metadata[0].name + } + + subject { + kind = "Group" + name = var.opssre_group_object_id + api_group = "rbac.authorization.k8s.io" + } +} diff --git a/quickstart/101-aks-entra-k8s-rbac/prequisite/main.tf b/quickstart/101-aks-entra-k8s-rbac/prequisite/main.tf new file mode 100644 index 000000000..f8ea968b3 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/prequisite/main.tf @@ -0,0 +1,37 @@ +data "azurerm_client_config" "current" {} + +resource "random_string" "suffix" { + length = 6 + special = false + upper = false +} + +resource "azurerm_resource_group" "rg" { + location = var.location + name = "rg-101-aks-entra-k8s-rbac-${random_string.suffix.result}" +} + +resource "azurerm_kubernetes_cluster" "aks" { + location = azurerm_resource_group.rg.location + name = "aks-101-entra-k8s-rbac-${random_string.suffix.result}" + resource_group_name = azurerm_resource_group.rg.name + dns_prefix = "aks-${random_string.suffix.result}" + # Kubernetes RBAC is required by the example, Azure RBAC for Kubernetes + # Authorization must stay disabled. + role_based_access_control_enabled = true + + default_node_pool { + name = "agentpool" + node_count = var.node_count + vm_size = "Standard_D2s_v3" + } + + identity { + type = "SystemAssigned" + } + + azure_active_directory_role_based_access_control { + azure_rbac_enabled = false + tenant_id = data.azurerm_client_config.current.tenant_id + } +} diff --git a/quickstart/101-aks-entra-k8s-rbac/prequisite/outputs.tf b/quickstart/101-aks-entra-k8s-rbac/prequisite/outputs.tf new file mode 100644 index 000000000..ee77e1927 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/prequisite/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + description = "Name of the resource group that contains the AKS cluster." + value = azurerm_resource_group.rg.name +} + +output "aks_cluster_name" { + description = "Name of the AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled." + value = azurerm_kubernetes_cluster.aks.name +} + +output "appdev_group_object_id" { + description = "Object ID of the existing test principal used for developer access." + value = data.azurerm_client_config.current.object_id +} + +output "opssre_group_object_id" { + description = "Object ID of the existing test principal used for SRE access." + value = azurerm_kubernetes_cluster.aks.identity[0].principal_id +} diff --git a/quickstart/101-aks-entra-k8s-rbac/prequisite/variables.tf b/quickstart/101-aks-entra-k8s-rbac/prequisite/variables.tf new file mode 100644 index 000000000..267eb51b2 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/prequisite/variables.tf @@ -0,0 +1,11 @@ +variable "location" { + type = string + default = "eastus" + description = "Location of the resources." +} + +variable "node_count" { + type = number + default = 1 + description = "Number of nodes in the default node pool of the AKS cluster." +} diff --git a/quickstart/101-aks-entra-k8s-rbac/prequisite/versions.tf b/quickstart/101-aks-entra-k8s-rbac/prequisite/versions.tf new file mode 100644 index 000000000..9a0ced2f0 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/prequisite/versions.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/test/e2e/quickstart_test.go b/test/e2e/quickstart_test.go index f818b5ac9..92d5dd9c5 100644 --- a/test/e2e/quickstart_test.go +++ b/test/e2e/quickstart_test.go @@ -26,6 +26,7 @@ var speicalTests = map[string]func(*testing.T){ "quickstart/202-machine-learning-moderately-secure-existing-VNet": test202machineLearningModeratelySecureExistingVnet, "quickstart/101-azure-netapp-files": test101AzureNetappFiles, "quickstart/101-azure-storage-actions-create-storage-task": test101AzureStorageActionsCreateStorageTask, + "quickstart/101-aks-entra-k8s-rbac": test101AksEntraK8sRbac, "quickstart/101-aks-use-azure-policy": test101AksUseAzurePolicy, } @@ -256,6 +257,26 @@ func test101AzureStorageActionsCreateStorageTask(t *testing.T) { }, nil) } +func test101AksEntraK8sRbac(t *testing.T) { + rootPath := filepath.Join("..", "..") + examplePath := filepath.Join("quickstart", "101-aks-entra-k8s-rbac") + prequistePath := filepath.Join(examplePath, "prequisite") + helper.RunE2ETestWithOption(t, rootPath, prequistePath, helper.TestOptions{ + SkipIdempotentCheck: true, + Assertion: func(t *testing.T, output helper.TerraformOutput) { + helper.RunE2ETest(t, rootPath, examplePath, terraform.Options{ + Upgrade: true, + Vars: map[string]interface{}{ + "resource_group_name": output["resource_group_name"], + "aks_cluster_name": output["aks_cluster_name"], + "appdev_group_object_id": output["appdev_group_object_id"], + "opssre_group_object_id": output["opssre_group_object_id"], + }, + }, nil) + }, + }) +} + func test201AksFleetManagedNamespaces(t *testing.T) { rootPath := filepath.Join("..", "..") examplePath := filepath.Join("quickstart", "201-aks-fleet-managed-namespaces")