github.com/Azure/aad-pod-identity@v1.8.17/examples/rest-api/README.md (about) 1 # Using AAD Pod Identity with a custom REST API 2 3 We will look at the scenario where a client pod is calling a service (REST API) with an AAD Pod Identity. 4 5 The client is a bash script running in a pod. The client has a corresponding user managed identity which will be exposed as an AAD Pod Identity. The client will request a token, pass it as authorization HTTP header to query a REST API. 6 7 The REST API is implemented in C# / .NET Core and is running in pod as well. It simply validates it receives a bearer token in the authorization header of each request. The REST API has a corresponding Azure AD Application. The client requests a token with that AAD application as the *resource*. 8 9  10 11 ## Prerequisites 12 13 * An AKS cluster with [AAD Pod Identity installed on it](https://github.com/Azure/aad-pod-identity/blob/master/README.md) 14 15 ## Identity 16 17 In this section, we'll create the user managed identity used for the client. 18 19 First, let's define those variable: 20 21 ```bash 22 rg=<name of the resource group where AKS is> 23 cluster=<name of the AKS cluster> 24 ``` 25 26 Then, let's create the user managed identity: 27 28 ```bash 29 az identity create -g $rg -n client-principal \ 30 --query "{ClientId: clientId, ManagedIdentityId: id, TenantId: tenantId}" -o jsonc 31 ``` 32 33 This returns three values in a JSON document. We will use those values later on. 34 35 We need to assign the Service Principal running the cluster the *Managed Identity Operator* role on the user managed identity: 36 37 ```bash 38 aksPrincipalId=$(az aks show -g $rg -n $cluster --query "servicePrincipalProfile.clientId" -o tsv) 39 managedId=$(az identity show -g $rg -n client-principal --query "id" -o tsv) 40 az role assignment create --role "Managed Identity Operator" \ 41 --assignee $aksPrincipalId --scope $managedId 42 ``` 43 44 The first line acquires the AKS service principal client ID. The second line acquires the client ID of the user managed identity (the *ManagedIdentityId* returned in the JSON above). The third line performs the role assignment. 45 46 ## Identity & Binding in Kubernetes 47 48 In this section, we'll configure AAD pod identity with the user managed identity. 49 50 We'll create a Kubernetes namespace to put all our resources. It makes it easier to clean up afterwards. 51 52 ```bash 53 kubectl create namespace pir 54 kubectl label namespace/pir description=PodIdentityRestApi 55 ``` 56 57 Let's customize [identity.yaml](identity.yaml): 58 59 ```yaml 60 apiVersion: "aadpodidentity.k8s.io/v1" 61 kind: AzureIdentity 62 metadata: 63 name: client-principal 64 spec: 65 type: 0 66 resourceID: <resource-id of client-principal> 67 clientID: <client-id of client-principal> 68 ``` 69 70 *ResourceID* should be set to the value of *ManagedIdentityId* in the JSON from the previous section. That is the resource ID of the user managed identity. 71 72 *ClientID* should be set to the value of *ClientId* in the JSON from the previous section. That is the client id of the user managed identity. 73 74 We **do not need to customize** [binding.yaml](binding.yaml). 75 76 ```yaml 77 apiVersion: "aadpodidentity.k8s.io/v1" 78 kind: AzureIdentityBinding 79 metadata: 80 name: client-principal-binding 81 spec: 82 azureIdentity: client-principal 83 selector: client-principal-pod-binding 84 ``` 85 86 We can now deploy those two files in the *pir* namespace: 87 88 ```bash 89 kubectl apply -f identity.yaml --namespace pir 90 kubectl apply -f binding.yaml --namespace pir 91 ``` 92 93 We can check the resources got deployed: 94 95 ```bash 96 $ kubectl get AzureIdentity --namespace pir 97 98 NAME AGE 99 client-principal 12s 100 101 $ kubectl get AzureIdentityBinding --namespace pir 102 103 NAME AGE 104 client-principal-binding 32s 105 ``` 106 107 ## Application 108 109 In this section, we will simply create the Azure AD application corresponding to the REST API Service: 110 111 ```bash 112 appId=$(az ad app create --display-name myapi \ 113 --identifier-uris http://myapi.restapi.aad-pod-identity \ 114 --query "appId" -o tsv) 115 echo $appId 116 ``` 117 118 The application's name is *myapi*. The identifier uri is irrelevant but required. 119 120 ## Client 121 122 In [client-pod.yaml](client-pod.yaml), we need to customize the value of the environment variable *RESOURCE* to the value of *$appId* computed above, i.e. the client id of the Azure AD application: 123 124 ```yaml 125 apiVersion: v1 126 kind: Pod 127 metadata: 128 name: aad-id-client-pod 129 labels: 130 app: aad-id-client 131 platform: cli 132 aadpodidbinding: client-principal-pod-binding 133 spec: 134 containers: 135 - name: main-container 136 image: vplauzon/aad-pod-id-client 137 env: 138 - name: RESOURCE 139 value: <Application Id> 140 - name: SERVICE_URL 141 value: http://aad-id-service 142 ``` 143 144 The client will use that when requesting a token. 145 146 The client's code is packaged in a container. The core of the code is [script.sh](client/script.sh): 147 148 ```bash 149 #!/bin/sh 150 151 echo "Hello ${RESOURCE}" 152 153 i=0 154 while true 155 do 156 echo "Iteration $i" 157 158 jwt=$(curl -sS http://169.254.169.254/metadata/identity/oauth2/token/?resource=$RESOURCE) 159 echo "Full token: $jwt" 160 token=$(echo $jwt | jq -r '.access_token') 161 echo "Access token: $token" 162 curl -v -H 'Accept: application/json' -H "Authorization: Bearer ${token}" $SERVICE_URL 163 164 i=$((i+1)) 165 sleep 1 166 done 167 ``` 168 169 Every second the script queries a token from http://169.254.169.254 which is exposed by AAD Pod Identity (more specifically the *Node Managed Identity* component, or NMI). 170 171 We use the [jq](https://stedolan.github.io/jq/) tool to parse the JSON of the token. We extract the access token element which we then pass as in an HTTP header using *curl*. 172 173 ## Service 174 175 In [service.yaml](service.yaml), we need to customize the value for APPLICATION_ID & TENANT_ID. 176 177 APPLICATION_ID's value is the same as the RESOURCE from previous section, i.e. *$appId*. TENANT_ID is the ID of the tenant owning the identities. It was given in the JSON as the output of the user managed identity. 178 179 ```yaml 180 apiVersion: v1 181 kind: Service 182 metadata: 183 name: aad-id-service 184 spec: 185 type: ClusterIP 186 ports: 187 - port: 80 188 selector: 189 app: aad-id-service 190 --- 191 apiVersion: v1 192 kind: Pod 193 metadata: 194 name: aad-id-service-pod 195 labels: 196 app: aad-id-service 197 platform: csharp 198 spec: 199 containers: 200 - name: api-container 201 image: vplauzon/aad-pod-id-svc 202 ports: 203 - containerPort: 80 204 env: 205 - name: TENANT_ID 206 value: <ID of TENANT owning the identities> 207 - name: APPLICATION_ID 208 value: <Application Id> 209 ``` 210 211 Here we defined a service and a pod implementing the service. The service is available on port 80. 212 213 [The code for the service](service) is packaged in a container. The core of it is [Startup.cs](service/MyApiSolution/MyApi/Startup.cs), more specifically its *ConfigureServices* method: 214 215 ```csharp 216 public void ConfigureServices(IServiceCollection services) 217 { 218 services 219 .AddAuthentication() 220 .AddJwtBearer(options => 221 { 222 options.Audience = _applicationId; 223 options.Authority = $"https://sts.windows.net/{_tenantId}/"; 224 }); 225 services.AddAuthorization(options => 226 { 227 var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder( 228 JwtBearerDefaults.AuthenticationScheme); 229 230 defaultAuthorizationPolicyBuilder = 231 defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); 232 options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build(); 233 }); 234 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 235 } 236 ``` 237 238 Here we specify that a [Json Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) is required for authentication as default. I.e. we do not need to put a *[Authentication]* attribute on controllers. 239 240 ## Test 241 242 Let's deploy the service & client. 243 244 ```bash 245 kubectl apply -f service.yaml --namespace pir 246 kubectl apply -f client-pod.yaml --namespace pir 247 kubectl get AzureAssignedIdentity --all-namespaces 248 ``` 249 250 The last command should return an entry. This is showing that Azure Pod Identity did bind an identity to a pod. 251 252 We can then monitor the client: 253 254 ```bash 255 kubectl logs aad-id-client-pod --namespace pir -f 256 ``` 257 258 Every second, we should see something like this: 259 260 ```bash 261 Iteration 6 262 Full token: {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCIsImtpZCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCJ9.eyJhdWQiOiI2ZmZhNjBiYy0yODZkLTRiMzAtOWMxYi0wMDBlODcyYjQxMzIiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUxMTkxMjM5LCJuYmYiOjE1NTExOTEyMzksImV4cCI6MTU1MTIyMDMzOSwiYWlvIjoiNDJKZ1lEanNuTTk4L0lqWDlQM1hmTU5zOW0zK0FnQT0iLCJhcHBpZCI6IjIxMWZlNTQ3LThhYzItNGE2Zi05MjYyLWFkZjMwNDM3ZTAzYiIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0Ny8iLCJvaWQiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJzdWIiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1dGkiOiJWMHdhSDQwbEtFS1RCQ29SUV82X0FBIiwidmVyIjoiMS4wIn0.I4hZusEV-A17rJSQW1CBH3oSVd0wedoXPVoPEXGxGdxYvXJCKYL1Mz0HoMJcp8s2_Z59Q16GHdvlWwnQNAhFIdfENF8Rj_jJ0ndh1ZtU1ioIPHWH461794bP38aUg-tBmxnfP-ZSjz-iiifx2n_iERTqlya4os80_WQm_OejW5kfrmpx6P7zcM5eqw7C_oKkM1l8BhhlDdwWnAc0pu5YWbMhgZBtJc9kzKh2IQyLmRkwbZEMJ-cJ_Xuay4jPccP-dGFylrC2KRQlWzovVraubX9vqkfL8f_eYPZIuhcUC1M2paf8wgQUyZWdvh-HzWKD5LdDCuvZCLeWpxaePTGhLA","refresh_token":"","expires_in":"28800","expires_on":"1551220339","not_before":"1551191239","resource":"6ffa60bc-286d-4b30-9c1b-000e872b4132","token_type":"Bearer"} 263 Access token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCIsImtpZCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCJ9.eyJhdWQiOiI2ZmZhNjBiYy0yODZkLTRiMzAtOWMxYi0wMDBlODcyYjQxMzIiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUxMTkxMjM5LCJuYmYiOjE1NTExOTEyMzksImV4cCI6MTU1MTIyMDMzOSwiYWlvIjoiNDJKZ1lEanNuTTk4L0lqWDlQM1hmTU5zOW0zK0FnQT0iLCJhcHBpZCI6IjIxMWZlNTQ3LThhYzItNGE2Zi05MjYyLWFkZjMwNDM3ZTAzYiIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0Ny8iLCJvaWQiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJzdWIiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1dGkiOiJWMHdhSDQwbEtFS1RCQ29SUV82X0FBIiwidmVyIjoiMS4wIn0.I4hZusEV-A17rJSQW1CBH3oSVd0wedoXPVoPEXGxGdxYvXJCKYL1Mz0HoMJcp8s2_Z59Q16GHdvlWwnQNAhFIdfENF8Rj_jJ0ndh1ZtU1ioIPHWH461794bP38aUg-tBmxnfP-ZSjz-iiifx2n_iERTqlya4os80_WQm_OejW5kfrmpx6P7zcM5eqw7C_oKkM1l8BhhlDdwWnAc0pu5YWbMhgZBtJc9kzKh2IQyLmRkwbZEMJ-cJ_Xuay4jPccP-dGFylrC2KRQlWzovVraubX9vqkfL8f_eYPZIuhcUC1M2paf8wgQUyZWdvh-HzWKD5LdDCuvZCLeWpxaePTGhLA 264 * Rebuilt URL to: http://aad-id-service/ 265 % Total % Received % Xferd Average Speed Time Time Time Current 266 Dload Upload Total Spent Left Speed 267 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 10.0.253.247... 268 * TCP_NODELAY set 269 * Connected to aad-id-service (10.0.253.247) port 80 (#0) 270 > GET / HTTP/1.1 271 > Host: aad-id-service 272 > User-Agent: curl/7.59.0 273 > Accept: application/json 274 > Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCIsImtpZCI6Ii1zeE1KTUxDSURXTVRQdlp5SjZ0eC1DRHh3MCJ9.eyJhdWQiOiI2ZmZhNjBiYy0yODZkLTRiMzAtOWMxYi0wMDBlODcyYjQxMzIiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUxMTkxMjM5LCJuYmYiOjE1NTExOTEyMzksImV4cCI6MTU1MTIyMDMzOSwiYWlvIjoiNDJKZ1lEanNuTTk4L0lqWDlQM1hmTU5zOW0zK0FnQT0iLCJhcHBpZCI6IjIxMWZlNTQ3LThhYzItNGE2Zi05MjYyLWFkZjMwNDM3ZTAzYiIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0Ny8iLCJvaWQiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJzdWIiOiI5YTU5NTgwNC00ZjJhLTRkZDItYjYzOC00YWVkZTY0MTU3OGQiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1dGkiOiJWMHdhSDQwbEtFS1RCQ29SUV82X0FBIiwidmVyIjoiMS4wIn0.I4hZusEV-A17rJSQW1CBH3oSVd0wedoXPVoPEXGxGdxYvXJCKYL1Mz0HoMJcp8s2_Z59Q16GHdvlWwnQNAhFIdfENF8Rj_jJ0ndh1ZtU1ioIPHWH461794bP38aUg-tBmxnfP-ZSjz-iiifx2n_iERTqlya4os80_WQm_OejW5kfrmpx6P7zcM5eqw7C_oKkM1l8BhhlDdwWnAc0pu5YWbMhgZBtJc9kzKh2IQyLmRkwbZEMJ-cJ_Xuay4jPccP-dGFylrC2KRQlWzovVraubX9vqkfL8f_eYPZIuhcUC1M2paf8wgQUyZWdvh-HzWKD5LdDCuvZCLeWpxaePTGhLA 275 > 276 < HTTP/1.1 200 OK 277 < Date: Tue, 26 Feb 2019 14:49:44 GMT 278 < Content-Type: application/json; charset=utf-8 279 < Server: Kestrel 280 < Transfer-Encoding: chunked 281 < 282 { [23 bytes data] 283 100 13 0 13 0 0 812 0 --:--:-- --:--:-- --:--:-- 812 284 * Connection #0 to host aad-id-service left intact 285 ``` 286 287 It shows the token being acquired, passed in the *Authorization* header as a *Bearer* token. The response is a 200, showing the authentication works. 288 289 Now we can verified the authentication is happening by removing it. Lets the logs be carried through. In a different terminal, let's run the following command: 290 291 ```bash 292 kubectl delete AzureIdentityBinding client-principal-binding --namespace pir 293 ``` 294 295 This basically delete the binding of the identity. As soon as that command returns, we should see the logs changing in something like: 296 297 ```bash 298 Iteration 269 299 Full token: no AzureAssignedIdentity found for pod:pir/aad-id-client-pod 300 parse error: Invalid literal at line 1, column 3 301 Access token: 302 * Rebuilt URL to: http://aad-id-service/ 303 % Total % Received % Xferd Average Speed Time Time Time Current 304 Dload Upload Total Spent Left Speed 305 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 10.0.253.247... 306 * TCP_NODELAY set 307 * Connected to aad-id-service (10.0.253.247) port 80 (#0) 308 > GET / HTTP/1.1 309 > Host: aad-id-service 310 > User-Agent: curl/7.59.0 311 > Accept: application/json 312 > Authorization: Bearer 313 > 314 < HTTP/1.1 401 Unauthorized 315 < Date: Tue, 26 Feb 2019 14:54:18 GMT 316 < Server: Kestrel 317 < Content-Length: 0 318 < WWW-Authenticate: Bearer 319 < 320 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 321 * Connection #0 to host aad-id-service left intact 322 ``` 323 324 We see the token isn't provided to the pod anymore. Since we do not pass a valid token, the service reject the call with a **401 Unauthorized**. 325 326 We can easily go back to previous state by running: 327 328 ```bash 329 kubectl apply -f binding.yaml --namespace pir 330 ``` 331 332 Again, as soon as the command returns the logs resume the **200 OK** response. 333 334 ## Clean up 335 336 In order to clean up both the Azure and Kubernetes resources we've been using, we can run the following commands: 337 338 ```bash 339 kubectl delete namespace pir 340 az identity delete -g $rg -n client-principal 341 az ad app delete --id $appId 342 ``` 343 344 The first line deletes the Kubernetes namespace and everything underneath it, i.e. identity, binding, client & service. The second line deletes the Azure User Managed Identity. The third line deletes the Azure AD application. 345 346 ## Notes 347 348 ### Build client docker container 349 sudo docker build -t vplauzon/aad-pod-id-client . 350 351 ### Publish client image 352 sudo docker push vplauzon/aad-pod-id-client 353 354 ### Build service docker container 355 sudo docker build -t vplauzon/aad-pod-id-svc . 356 357 ### Publish service image 358 sudo docker push vplauzon/aad-pod-id-svc 359