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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions quickstart/101-aks-entra-k8s-rbac/README.md
Original file line number Diff line number Diff line change
@@ -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 Azure Kubernetes Service (AKS) cluster that the configuration creates.

The example creates two Microsoft Entra groups and scopes each group to a namespace:

- The `appdev` group can manage resources in the `dev` namespace.
- The `opssre` group can manage resources in the `sre` namespace.

The AKS cluster is created with Microsoft Entra integration and Kubernetes RBAC enabled, and with Azure RBAC for Kubernetes Authorization disabled, which this example requires.

## Prerequisites

- An Azure subscription
- Permission to create resource groups and AKS clusters in the subscription
- Terraform `>= 1.6.0` installed
- Azure CLI and `kubectl` installed
- Permission to create Microsoft Entra users and groups
- Permission to assign Azure roles at the AKS cluster scope
- Permission to manage Kubernetes resources on the AKS cluster

Sign in to Azure and select the subscription to use:

```console
az login
az account set --subscription <subscription-id>
```

## Terraform providers and variables

This sample uses the AzureRM provider to create the resource group and the AKS cluster and to assign Azure permissions, the AzureAD provider to create Microsoft Entra users and groups, and the Kubernetes provider to create namespaces, Roles, and RoleBindings.

All Terraform variables have defaults, so the configuration runs without any input. Override them if you need a different location, resource group name prefix, or node count:

```hcl
resource_group_location = "eastus"
resource_group_name_prefix = "rg"
node_count = 1
```

The Microsoft Entra group names, user principal names, and the temporary password for the example users are generated, so the configuration can be applied repeatedly in the same tenant without name collisions. The `appdev` and `opssre` group names and the example user principal names each carry the same generated suffix, and the `appdev_group_object_id` and `opssre_group_object_id` outputs report the object IDs of the two groups. The generated password is never written to an output.

## Example

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, Microsoft Entra, and Kubernetes resources:

- A resource group and an AKS cluster with Microsoft Entra integration and Kubernetes RBAC
- `appdev` and `opssre` Microsoft Entra security groups
- One example Microsoft Entra user in each group
- 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 $(terraform output -raw resource_group_name) \
--name $(terraform output -raw aks_cluster_name)
```

Verify that both namespaces exist:

```console
kubectl get namespaces
```

The output should include `dev` and `sre`.

## Test appdev access

Authenticate as the `appdev` test user 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 the `opssre` test user 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 resource group, AKS cluster, namespaces, RoleBindings, Roles, groups, users, and role assignments created by this configuration:

```console
terraform destroy
```
256 changes: 256 additions & 0 deletions quickstart/101-aks-entra-k8s-rbac/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
terraform {
required_version = ">= 1.6.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 3.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.30"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}

provider "azurerm" {
features {}
}

provider "azuread" {}

variable "resource_group_location" {
type = string
default = "eastus"
description = "Location of the resource group."
}

variable "resource_group_name_prefix" {
type = string
default = "rg"
description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription."
}

variable "node_count" {
type = number
default = 1
description = "Number of nodes in the AKS default node pool."
}

resource "random_pet" "rg_name" {
prefix = var.resource_group_name_prefix
}

resource "random_string" "suffix" {
length = 8
numeric = false
special = false
upper = false
}

resource "random_password" "temporary_password" {
length = 24
min_lower = 2
min_upper = 2
min_numeric = 2
min_special = 2
}

data "azurerm_client_config" "current" {}

data "azuread_domains" "default" {
only_initial = true
}

resource "azurerm_resource_group" "rg" {
name = random_pet.rg_name.id
location = var.resource_group_location
}

resource "azurerm_kubernetes_cluster" "aks" {
name = "aks-${random_string.suffix.result}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
dns_prefix = "aks-${random_string.suffix.result}"
role_based_access_control_enabled = true

default_node_pool {
name = "default"
node_count = var.node_count
vm_size = "Standard_DS2_v2"
}

identity {
type = "SystemAssigned"
}

azure_active_directory_role_based_access_control {
tenant_id = data.azurerm_client_config.current.tenant_id
azure_rbac_enabled = false
}
}

provider "kubernetes" {
host = azurerm_kubernetes_cluster.aks.kube_admin_config[0].host
client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_certificate)
client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_key)
cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate)
}

resource "azuread_group" "appdev" {
display_name = "appdev-${random_string.suffix.result}"
security_enabled = true
}

resource "azuread_group" "opssre" {
display_name = "opssre-${random_string.suffix.result}"
security_enabled = true
}

resource "azuread_user" "aksdev" {
user_principal_name = "aksdev-${random_string.suffix.result}@${data.azuread_domains.default.domains[0].domain_name}"
display_name = "AKS Dev"
mail_nickname = "aksdev-${random_string.suffix.result}"
password = random_password.temporary_password.result
}

resource "azuread_user" "akssre" {
user_principal_name = "akssre-${random_string.suffix.result}@${data.azuread_domains.default.domains[0].domain_name}"
display_name = "AKS SRE"
mail_nickname = "akssre-${random_string.suffix.result}"
password = random_password.temporary_password.result
}

resource "azuread_group_member" "appdev_member" {
group_object_id = azuread_group.appdev.object_id
member_object_id = azuread_user.aksdev.object_id
}

resource "azuread_group_member" "opssre_member" {
group_object_id = azuread_group.opssre.object_id
member_object_id = azuread_user.akssre.object_id
}

resource "azurerm_role_assignment" "appdev_cluster_user" {
scope = azurerm_kubernetes_cluster.aks.id
role_definition_name = "Azure Kubernetes Service Cluster User Role"
principal_id = azuread_group.appdev.object_id
}

resource "azurerm_role_assignment" "opssre_cluster_user" {
scope = azurerm_kubernetes_cluster.aks.id
role_definition_name = "Azure Kubernetes Service Cluster User Role"
principal_id = azuread_group.opssre.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 = azuread_group.appdev.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 = azuread_group.opssre.object_id
api_group = "rbac.authorization.k8s.io"
}
}

output "appdev_group_object_id" {
value = azuread_group.appdev.object_id
}

output "opssre_group_object_id" {
value = azuread_group.opssre.object_id
}

output "resource_group_name" {
value = azurerm_resource_group.rg.name
}

output "aks_cluster_name" {
value = azurerm_kubernetes_cluster.aks.name
}
Loading