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  ![Solution Overview](images/pod-identity-rest.png)
    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