Skip to content

Do not MERGE: MSI v2 Sample: working example for MSIv2 #558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

4gust
Copy link
Collaborator

@4gust 4gust commented Mar 4, 2025

Goals

The primary objective is to enable seamless token acquisition in MSI V2 for VM/VMSS, utilizing the /credential endpoint.

  • Define the MSI V2 token acquisition process.
  • Describe how MSAL interacts with the /credential and the ESTS regional token endpoint.
  • Ensure compatibility with Windows and Linux VMs and VMSS.

Token Acquisition Process

In MSI V1, IMDS or any other Managed Identity Resource Provider (MIRP) directly returns an access token. However, in MSI V2, the process involves two steps:

sequenceDiagram
    participant Application
    participant MSAL
    participant IMDS
    participant ESTS

    Application ->> MSAL: 1. Request token using Managed Identity
    MSAL ->> IMDS: 2. Probe for `/credential` endpoint availability
    IMDS -->> MSAL: 3. Response (200 OK / 404 Not Found)

    alt `/credential` endpoint available
        MSAL ->> IMDS: 4. Request Short-Lived Credential (SLC) via `/credential`
        IMDS -->> MSAL: 5. Return SLC
        MSAL ->> ESTS: 6. Exchange SLC for Access Token via MTLS
        ESTS -->> MSAL: 7. Return Access Token
        MSAL ->> Application: 8. Return Access Token
    else `/credential` endpoint not available
        MSAL ->> IMDS: 4a. Fallback to legacy `/token` endpoint
        IMDS -->> MSAL: 5a. Return Access Token
        MSAL ->> Application: 6a. Return Access Token
    end
Loading

Short-Lived Credential Retrieval from /credential Endpoint

  • Azure Managed Identity Resource Providers host the /credential endpoint.
  • The client (MSAL) calls the /credential endpoint to retrieve a short-lived credential (SLC).
  • This credential is valid for a short duration and must be used promptly in the next step.

Access Token Acquisition via ESTS

  • The client presents the short-lived credential to ESTS over MTLS as an assertion.
  • ESTS validates the credential and issues an access token.
  • The access token is then used to authenticate with Azure services.

Retry Logic

MSAL uses the default Managed Identity retry policy for MSI V2 credential/token requests, whether calling the ESTS endpoint or the new /credential endpoint. i.e. MSAL performs 3 retries with a 1 second pause between each retry. Retries are performed on certain error codes only.

Steps for MSI V2 Authentication

This section outlines the necessary steps to acquire an access token using the MSI V2 /credential endpoint.

1. Check for an Existing (Platform) Certificate (Windows only)

  • Search for a specific certificate (devicecert.mtlsauth.local) in (Cert:\LocalMachine\My).
  • If the certificate is not found in Local Machine, check Current User's certificate store (Cert:\CurrentUser\My).

2. Generate a New Certificate (if platform certificate is not found)

  • If no valid platform certificate is found in Cert:\LocalMachine\My or Cert:\CurrentUser\My, create a new in-memory self-signed certificate.
  • This applies especially to Linux VMs, where platform certificates are not pre-configured, and MSAL must always generate an in-memory certificate for MTLS authentication.

Certificate Creation Requirements

  • Subject Name: CN=mtls-auth (subject name not final).
  • Validity Period: 90 days.
  • Key Export Policy: Private key must be exportable to allow use for MTLS authentication.
  • Key Usage must include: Digital Signature, Key Encipherment and TLS Client Authentication.
  • Storage: The certificate should exist only in memory. It is not stored in the certificate store. It is discarded when the process exits.

Certificate Rotation Strategy

  • Track Expiry: The expiration of the certificate must be monitored at runtime.
  • Rotation Trigger: 5 days before expiry, generate a new in-memory certificate.

3. Extract Certificate Data

  • Convert the certificate to a Base64-encoded string (x5c).
  • Format the JSON payload containing the certificate details for request authentication.

4. Request MSI Credential

  • Send a POST request to the IMDS /credential endpoint with the certificate details.
  • The request must include:
    • Metadata: true header.
    • X-ms-Client-Request-id header with a GUID.
    • JSON body containing the certificate's public key in jwk format. RFC
  • Parse the response to extract:
    • regional_token_url
    • tenant_id
    • client_id
    • credential (short-lived credential).

5. Request Access Token from ESTS

  • Construct the OAuth2 request body, including:
    • grant_type=client_credentials
    • scope=https://management.azure.com/.default
    • client_id from the MSI response.
    • client_assertion containing the short-lived credential.
    • client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
  • Send a POST request to the regional_token_url with the certificate for mutual TLS (mTLS) authentication.

6. Retrieve and Use Access Token

  • Parse the response to extract the access_token.
  • Use the access token to authenticate requests to Azure services.
  • Handle any errors that may occur during the token request.

End-to-End Script

# Define certificate subject names
$searchSubject = "CN=devicecert.mtlsauth.local"  # Existing cert to look for
$newCertSubject = "CN=mtls-auth"  # Subject for new self-signed cert

# Step 1: Search for an existing certificate in LocalMachine\My
$cert = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -eq $searchSubject -and $_.NotAfter -gt (Get-Date) }

# Step 2: If not found, search in CurrentUser\My
if (-not $cert) {
    Write-Output "🔍 No valid certificate found in LocalMachine\My. Checking CurrentUser\My..."
    $cert = Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object { $_.Subject -eq $searchSubject -and $_.NotAfter -gt (Get-Date) }
}

# Step 3: If found, use it
if ($cert) {
    Write-Output "✅ Found valid certificate: $($cert.Subject)"
} else {
    Write-Output "❌ No valid certificate found in both stores. Creating a new self-signed certificate in `CurrentUser\My`..."

    # Step 4: Generate a new self-signed certificate in `CurrentUser\My`
    # For POC we are creating the cert in the user store. But in Product this will be a in-memory cert
    $cert = New-SelfSignedCertificate `
        -Subject $newCertSubject `
        -CertStoreLocation "Cert:\CurrentUser\My" `
        -KeyExportPolicy Exportable `
        -KeySpec Signature `
        -KeyUsage DigitalSignature, KeyEncipherment `
        -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2") `
        -NotAfter (Get-Date).AddDays(90)

    Write-Output "✅ Created certificate in CurrentUser\My: $($cert.Thumbprint)"
}

# Ensure `$cert` is valid
if (-not $cert) {
    Write-Error "❌ No certificate found or created. Exiting."
    exit
}

# Step 5: Compute SHA-256 of the Public Key for `kid`
$publicKeyBytes = $cert.GetPublicKey()
$sha256 = New-Object System.Security.Cryptography.SHA256Managed
$certSha256 = [BitConverter]::ToString($sha256.ComputeHash($publicKeyBytes)) -replace "-", ""

Write-Output "🔐 Using SHA-256 Certificate Identifier (kid): $certSha256"

# Step 6: Convert certificate to Base64 for JWT (x5c field)
$x5c = [System.Convert]::ToBase64String($cert.RawData)
Write-Output "📜 x5c: $x5c"

# Step 7: Construct the JSON body properly
$bodyObject = @{
    cnf = @{
        jwk = @{
            kty = "RSA"
            use = "sig"
            alg = "RS256"
            kid = $certSha256  # Use SHA-256 instead of Thumbprint
            x5c = @($x5c)  # Ensures correct array formatting
        }
    }
    latch_key = $false  # Final version of the product should not have this. IMDS team is working on removing this. 
}

# Convert JSON object to a string
$body = $bodyObject | ConvertTo-Json -Depth 10 -Compress
Write-Output "🔹 JSON Payload: $body"

# Step 8: Request MSI credential
$headers = @{
    "Metadata" = "true"
    "X-ms-Client-Request-id" = [guid]::NewGuid().ToString()
}

$imdsResponse = Invoke-WebRequest -Uri "http://169.254.169.254/metadata/identity/credential?cred-api-version=1.0" `
    -Method POST `
    -Headers $headers `
    -Body $body

$jsonContent = $imdsResponse.Content | ConvertFrom-Json

$regionalEndpoint = $jsonContent.regional_token_url + "/" + $jsonContent.tenant_id + "/oauth2/v2.0/token"
Write-Output "✅ Using Regional Endpoint: $regionalEndpoint"

# Step 9: Authenticate with Azure
$tokenHeaders = @{
    "Content-Type" = "application/x-www-form-urlencoded"
    "Accept" = "application/json"
}

$tokenRequestBody = "grant_type=client_credentials&scope=https://management.azure.com/.default&client_id=$($jsonContent.client_id)&client_assertion=$($jsonContent.credential)&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

try {
    $tokenResponse = Invoke-WebRequest -Uri $regionalEndpoint `
        -Method POST `
        -Headers $tokenHeaders `
        -Body $tokenRequestBody `
        -Certificate $cert  # Use the full certificate object

    $tokenJson = $tokenResponse.Content | ConvertFrom-Json
    Write-Output "🔑 Access Token: $($tokenJson.access_token)"
} catch {
    Write-Error "❌ Failed to retrieve access token. Error: $_"
}

This is just replication of what is in the .ps script.

@4gust 4gust requested review from bgavrilMS and rayluo as code owners March 4, 2025 17:33
Copy link

sonarqubecloud bot commented Mar 4, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
16.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@4gust 4gust changed the title MSI v2 Sample: working example for MSIv2 Do not MERGE: MSI v2 Sample: working example for MSIv2 Mar 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant