Repository: smallstep/cli
Pull request #646 (2022-03-07 12:04:10)

WIP: Support Azure tokens from managed identities not associated with a VM
I know that its expected that users utilise either the ACME provisioner or I guess the K8sSA provisioner on Kubernetes. However, I think it would still be nice if it were possible to use step cli in container on Kubernetes, specifically on Azure Kubernetes Service (AKS).

At the moment it's not possible because the value in the xms_mirid claim is expected to match the regular expression:

var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachines/([^/]+)$`)                 
However, on AKS you can use [Azure AD Pod Identity](https://docs.microsoft.com/en-us/azure/aks/use-azure-ad-pod-identity) the purpose of this is to allow you to use a managed identity to access Azure resources from within workloads running on AKS. When you use this the token you obtain from the IMDS has an xms_mirid claim that looks like this:

var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.ManagedIdentity/userAssignedIdentities/([^/]+)$`)  
Note the xms_mirid claim looks like this for VM Scale Sets as well.

The net result is that today if you try to run

step ca certificate <somedomain> mycert.crt mykey.key --provisioner "Azure"                                                                                             
You receive the error token is not supported.

The regular expression is used in authority/provisioner/azure.go in [authorizeToken](https://github.com/smallstep/certificates/blob/master/authority/provisioner/azure.go#L265) and in [parseResponse](https://github.com/smallstep/cli/blob/master/token/parse.go#L174) in the step cli. Furthermore, in parseResponse the virtual machine name is extracted from this xms_mirid claim as it is used as a default SAN in [CreateSignRequest](https://github.com/smallstep/cli/blob/master/utils/cautils/certificate_flow.go#L274).

This PR changes the regular expression used for the xms_mirid claim so it can accept both types of resource identifiers. However, there is a side effect that the virtual machine name is used as the default SAN - in particular when DisableCustomSANs is true, the IP address and the VM name are added as SANs. But when the resource type is Microsoft.ManagedIdentity we won't have a real VM name so I have assumed in this case that the virtual machine name should just be the empty string.

I think it also means that if custom SANs are disabled then it won't issue certs/authorize the token when the resource type is Managed.Identity.

Please let me know if people think we should support this scenario in a different way (or indeed if the feeling is either the ACME or K8sSA provisioner should be used instead). I've marked it as WIP because I think I'd probably need to add new tests but I wanted to see if the project is open to making a change like this.

This is the associated [PR](https://github.com/smallstep/certificates/pull/845) for the certificates repo.


Patch:
diff --git a/token/parse.go b/token/parse.go
index afdad9d6..8a005481 100644
--- a/token/parse.go
+++ b/token/parse.go
@@ -125,13 +125,14 @@ type AWSInstanceIdentityDocument struct {
 
 // azureXMSMirIDRegExp is the regular expression used to parse the xms_mirid claim.
 // Using case insensitive as resourceGroups appears as resourcegroups.
-var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.Compute/virtualMachines/([^/]+)$`)
+var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.(Compute/virtualMachines|ManagedIdentity/userAssignedIdentities)/([^/]+)$`)
 
 // AzurePayload contains the information in the xms_mirid claim.
 type AzurePayload struct {
 	SubscriptionID string
 	ResourceGroup  string
-	VirtualMachine string
+	ResourceType   string
+	ResourceName   string
 }
 
 // Parse parses the given token verifying the signature with the key.
@@ -175,7 +176,8 @@ func parseResponse(jwt *jose.JSONWebToken, p Payload) (*JSONWebToken, error) {
 			p.Azure = &AzurePayload{
 				SubscriptionID: re[1],
 				ResourceGroup:  re[2],
-				VirtualMachine: re[3],
+				ResourceType:   strings.Split(re[3], "/")[1],
+				ResourceName:   re[4],
 			}
 		}
 	}
diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go
index 6efc9aee..6cd664cb 100644
--- a/utils/cautils/certificate_flow.go
+++ b/utils/cautils/certificate_flow.go
@@ -271,7 +271,7 @@ func (f *CertificateFlow) CreateSignRequest(ctx *cli.Context, tok, subject strin
 	case token.Azure:
 		if len(ips) == 0 && len(dnsNames) == 0 {
 			defaultSANs := []string{
-				jwt.Payload.Azure.VirtualMachine,
+				jwt.Payload.Azure.ResourceName,
 			}
 			if !sharedContext.DisableCustomSANs {
 				defaultSANs = append(defaultSANs, subject)


Test patch:
diff --git a/token/parse_test.go b/token/parse_test.go
index caa7daf1..6e20ecbc 100644
--- a/token/parse_test.go
+++ b/token/parse_test.go
@@ -47,9 +47,13 @@ const (
 	  }
 	]
 }`
-	awsToken   = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbWF6b24iOnsiZG9jdW1lbnQiOiJld29nSUNKaFkyTnZkVzUwU1dRaU9pQWlTVzV0UWxOMGVuaDJjeUlzQ2lBZ0ltRnlZMmhwZEdWamRIVnlaU0k2SUNKNE9EWmZOalFpTEFvZ0lDSmhkbUZwYkdGaWFXeHBkSGxhYjI1bElqb2dJblZ6TFhkbGMzUXRNbUlpTEFvZ0lDSmlhV3hzYVc1blVISnZaSFZqZEhNaU9pQnVkV3hzTEFvZ0lDSmtaWFp3WVhsUWNtOWtkV04wUTI5a1pYTWlPaUJ1ZFd4c0xBb2dJQ0pwYldGblpVbGtJam9nSWxOQk5IaFpWVEZGWWtRaUxBb2dJQ0pwYm5OMFlXNWpaVWxrSWpvZ0lsb3pXRmRzV0VkT1dsSWlMQW9nSUNKcGJuTjBZVzVqWlZSNWNHVWlPaUFpZERJdWJXbGpjbThpTEFvZ0lDSnJaWEp1Wld4SlpDSTZJQ0lpTEFvZ0lDSndaVzVrYVc1blZHbHRaU0k2SUNJeU1ERTVMVEExTFRBNVZEQXhPakEwT2pJekxqVXlORFkyT1ZvaUxBb2dJQ0p3Y21sMllYUmxTWEFpT2lBaU1USTNMakF1TUM0eElpd0tJQ0FpY21GdFpHbHphMGxrSWpvZ0lpSXNDaUFnSW5KbFoybHZiaUk2SUNKMWN5MTNaWE4wTFRFaUxBb2dJQ0oyWlhKemFXOXVJam9nSWpJd01UY3RNRGt0TXpBaUNuMD0iLCJzaWduYXR1cmUiOiJKbkdlaWl2UzMrU1QxNGdFd2NCQS9MK0dHYkRyZm1iWmh3dU9tMjIrbFhqWjZzSUN4YUpRaFg1bmxyRDBrZUlvL2Y0dmRkT3FGTzhvUWNHd0J1YXl6aHFSVEt6cGpZVXBoSDFRMlJBUGl4dmcvZ0NSZG5YV0pETHpIRGhjTXdraCtvWnpFTkFuSTlhWGR4Vko4dGFKSlRHZ2lEVzdBcXYzK1JYZG4xcndiVVE9In0sImF1ZCI6WyJhd3M6TGdONHU4OUliNyJdLCJleHAiOjE1NTczNjQxNjMsImlhdCI6MTU1NzM2Mzg2MywiaXNzIjoiZWMyLmFtYXpvbmF3cy5jb20iLCJqdGkiOiI2ZDAyZDQ1MTEzODUzOTc3OTRmMmJkNjc0M2I1Nzc0MmIyNDMzYzE1Y2ZhNjEzZGNjYWYwNmNiMzUzOGY5YzJmIiwibmJmIjoxNTU3MzYzODYzLCJzYW5zIjpudWxsLCJzdWIiOiJaM1hXbFhHTlpSIn0.bP40Dk1YMQlNMK_XYVde93x53c92hCwYCsUp4tL7wrA"
+	awsToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbWF6b24iOnsiZG9jdW1lbnQiOiJld29nSUNKaFkyTnZkVzUwU1dRaU9pQWlTVzV0UWxOMGVuaDJjeUlzQ2lBZ0ltRnlZMmhwZEdWamRIVnlaU0k2SUNKNE9EWmZOalFpTEFvZ0lDSmhkbUZwYkdGaWFXeHBkSGxhYjI1bElqb2dJblZ6TFhkbGMzUXRNbUlpTEFvZ0lDSmlhV3hzYVc1blVISnZaSFZqZEhNaU9pQnVkV3hzTEFvZ0lDSmtaWFp3WVhsUWNtOWtkV04wUTI5a1pYTWlPaUJ1ZFd4c0xBb2dJQ0pwYldGblpVbGtJam9nSWxOQk5IaFpWVEZGWWtRaUxBb2dJQ0pwYm5OMFlXNWpaVWxrSWpvZ0lsb3pXRmRzV0VkT1dsSWlMQW9nSUNKcGJuTjBZVzVqWlZSNWNHVWlPaUFpZERJdWJXbGpjbThpTEFvZ0lDSnJaWEp1Wld4SlpDSTZJQ0lpTEFvZ0lDSndaVzVrYVc1blZHbHRaU0k2SUNJeU1ERTVMVEExTFRBNVZEQXhPakEwT2pJekxqVXlORFkyT1ZvaUxBb2dJQ0p3Y21sMllYUmxTWEFpT2lBaU1USTNMakF1TUM0eElpd0tJQ0FpY21GdFpHbHphMGxrSWpvZ0lpSXNDaUFnSW5KbFoybHZiaUk2SUNKMWN5MTNaWE4wTFRFaUxBb2dJQ0oyWlhKemFXOXVJam9nSWpJd01UY3RNRGt0TXpBaUNuMD0iLCJzaWduYXR1cmUiOiJKbkdlaWl2UzMrU1QxNGdFd2NCQS9MK0dHYkRyZm1iWmh3dU9tMjIrbFhqWjZzSUN4YUpRaFg1bmxyRDBrZUlvL2Y0dmRkT3FGTzhvUWNHd0J1YXl6aHFSVEt6cGpZVXBoSDFRMlJBUGl4dmcvZ0NSZG5YV0pETHpIRGhjTXdraCtvWnpFTkFuSTlhWGR4Vko4dGFKSlRHZ2lEVzdBcXYzK1JYZG4xcndiVVE9In0sImF1ZCI6WyJhd3M6TGdONHU4OUliNyJdLCJleHAiOjE1NTczNjQxNjMsImlhdCI6MTU1NzM2Mzg2MywiaXNzIjoiZWMyLmFtYXpvbmF3cy5jb20iLCJqdGkiOiI2ZDAyZDQ1MTEzODUzOTc3OTRmMmJkNjc0M2I1Nzc0MmIyNDMzYzE1Y2ZhNjEzZGNjYWYwNmNiMzUzOGY5YzJmIiwibmJmIjoxNTU3MzYzODYzLCJzYW5zIjpudWxsLCJzdWIiOiJaM1hXbFhHTlpSIn0.bP40Dk1YMQlNMK_XYVde93x53c92hCwYCsUp4tL7wrA"
+	// azureToken represents a token obtained by a VM from the Azure IMDS (where the resource type is Microsoft.Compute/virtualMachines)
 	azureToken = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjY2YjQxZjFlOTNmYTM5YTQzMjVkNmFhYmNlZGYwODJhZjg0NGZiMjJlZTk1NDAzYmVmOWQ0ZGNmMzcwYjcxMGUiLCJ0eXAiOiJKV1QifQ.eyJhcHBpZCI6InRoZS1hcHBpZCIsImFwcGlkYWNyIjoidGhlLWFwcGlkYWNyIiwiYXVkIjpbImh0dHBzOi8vbWFuYWdlbWVudC5henVyZS5jb20vIl0sImV4cCI6MTU1NzM0NTY1OCwiaWF0IjoxNTU3MzQ1MzU4LCJpZHAiOiJ0aGUtaWRwIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvUDg3aEFXVUUwYy8iLCJqdGkiOiJ0aGUtanRpIiwibmJmIjoxNTU3MzQ1MzU4LCJvaWQiOiJ0aGUtb2lkIiwic3ViIjoic3ViamVjdCIsInRpZCI6IlA4N2hBV1VFMGMiLCJ2ZXIiOiJ0aGUtdmVyc2lvbiIsInhtc19taXJpZCI6Ii9zdWJzY3JpcHRpb25zL3N1YnNjcmlwdGlvbklEL3Jlc291cmNlR3JvdXBzL3Jlc291cmNlR3JvdXAvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21wdXRlL3ZpcnR1YWxNYWNoaW5lcy92aXJ0dWFsTWFjaGluZSJ9.U_NdLMXLztkYEn0RXYD394SiR-QpaJ8eoRFBu0FYQJ6zDflc-veAceFbLsTdihNE21zm7qYAkDqXlE0q3Y7Zdg"
-	badToken   = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
+	// azureTokenMI represents a token obtained by a ManagedIdentity from the Azure IMDS
+	// This type of token has a different resource id (specifically the resource type is Microsoft.ManagedIdentity/userAssignedIdentities)
+	azureTokenMI = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjY2YjQxZjFlOTNmYTM5YTQzMjVkNmFhYmNlZGYwODJhZjg0NGZiMjJlZTk1NDAzYmVmOWQ0ZGNmMzcwYjcxMGUiLCJ0eXAiOiJKV1QifQ.eyJhcHBpZCI6InRoZS1hcHBpZCIsImFwcGlkYWNyIjoidGhlLWFwcGlkYWNyIiwiYXVkIjpbImh0dHBzOi8vbWFuYWdlbWVudC5henVyZS5jb20vIl0sImV4cCI6MTU1NzM0NTY1OCwiaWF0IjoxNTU3MzQ1MzU4LCJpZHAiOiJ0aGUtaWRwIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvUDg3aEFXVUUwYy8iLCJqdGkiOiJ0aGUtanRpIiwibmJmIjoxNTU3MzQ1MzU4LCJvaWQiOiJ0aGUtb2lkIiwic3ViIjoic3ViamVjdCIsInRpZCI6IlA4N2hBV1VFMGMiLCJ2ZXIiOiJ0aGUtdmVyc2lvbiIsInhtc19taXJpZCI6Ii9zdWJzY3JpcHRpb25zL3N1YnNjcmlwdGlvbklEL3Jlc291cmNlR3JvdXBzL3Jlc291cmNlR3JvdXAvcHJvdmlkZXJzL01pY3Jvc29mdC5NYW5hZ2VkSWRlbnRpdHkvdXNlckFzc2lnbmVkSWRlbnRpdGllcy9tYW5hZ2VkSWRlbnRpdHlOYW1lIn0K.U_NdLMXLztkYEn0RXYD394SiR-QpaJ8eoRFBu0FYQJ6zDflc-veAceFbLsTdihNE21zm7qYAkDqXlE0q3Y7Zdg"
+	badToken     = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyeyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
 )
 
 func TestParse(t *testing.T) {
@@ -239,7 +243,24 @@ func TestParseInsecure(t *testing.T) {
 			Azure: &AzurePayload{
 				SubscriptionID: "subscriptionID",
 				ResourceGroup:  "resourceGroup",
-				VirtualMachine: "virtualMachine",
+				ResourceType:   "virtualMachines",
+				ResourceName:   "virtualMachine",
+			},
+		}, false},
+		{"ok Azure", args{azureTokenMI}, Payload{
+			Claims:           mustJoseClaims(azureToken),
+			AppID:            "the-appid",
+			AppIDAcr:         "the-appidacr",
+			IdentityProvider: "the-idp",
+			ObjectID:         "the-oid",
+			TenantID:         "P87hAWUE0c",
+			Version:          "the-version",
+			XMSMirID:         "/subscriptions/subscriptionID/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/managedIdentityName",
+			Azure: &AzurePayload{
+				SubscriptionID: "subscriptionID",
+				ResourceGroup:  "resourceGroup",
+				ResourceType:   "userAssignedIdentities",
+				ResourceName:   "managedIdentityName",
 			},
 		}, false},
 		{"fail bad token", args{"foobarzar"}, Payload{}, true},