Skip to content

Instantly share code, notes, and snippets.

@maskati
Last active August 29, 2024 14:53
Show Gist options
  • Save maskati/e0d74330dcf15848b043825cf6b2f8b7 to your computer and use it in GitHub Desktop.
Save maskati/e0d74330dcf15848b043825cf6b2f8b7 to your computer and use it in GitHub Desktop.
Emulate Azure managed identity locally

Here is how you can emulate Azure managed identity on your local system by running an instance metadata service identity endpoint and simulating the token API to return an access token from Azure CLI. You can use this for example to log into SQL Server Management studio using your Azure CLI identity, or locally test code that uses the "Azure only" ManagedIdentityCredential.

managed_identity_emulator

Warning

Azure AD tokens are security sensitive material. Even though this script only exposes tokens on a local loopback interface, ensure you understand the consequences of doing this.

Note

The below script is for Windows and has been tested on Windows 11 and PowerShell 7.3. The same mechanism should work on other platforms with some modifications.

You must first bind the well-known and link-local (non-routable) instance metadata service IP address 169.254.169.254 to your loopback interface. Using the loopback interface ensures the tokens can only be acquired from processes on your local system. On Windows in an elevated terminal:

# configure imds well known ip address on loopback interface in the active (until next reboot) store
netsh interface ipv4 add address name=1 address=169.254.169.254 mask=255.255.255.255 store=active skipassource=true
# show the loopback interface ip address configuration
netsh interface ipv4 show addresses name=1

Ensure you are logged into Azure CLI.

Run the following script in an elevated terminal, which will start up an HttpListener that emulates the IMDS identity endpoint and returns tokens using your Azure CLI logged in identity. The terminal needs to be elevated for HttpListener to bind to the IP address without a namespace reservation.

& {
	clear-host
	$account = az account show|convertfrom-json
	if($null -eq $account) {write-warning 'Azure CLI not logged into any accounts';break;}
	write-host -f green "Returning tokens from account $($account.name) ($($account.id)) for principal $($account.user.name) of type $($account.user.type)"
	$listener = new-object net.httplistener
	$listener.prefixes.add("http://169.254.169.254:80/")
	$listener.start()
	write-host -f green "Started IMDS token endpoint on $($listener.prefixes)"
	$cache = [runtime.caching.memorycache]::new('tokens')
	try {
		while($listener.islistening) {
			$task = $listener.getcontextasync()
			$taskid = $task.id
			$startdate = get-date
			while(-not $task.wait(1000)){write-progress -id $taskid -activity "Waiting for connection $taskid (CTRL-C to stop)" -status "$([int]((get-date)-$startdate).totalseconds) seconds"}
			write-progress -id $taskid -activity "Waiting for connection $taskid" -completed
			if ($task.iscompletedsuccessfully) {
				$context = $task.result
				$request = $context.request
				$response = $context.response
				write-host -f cyan "$($taskid): received request from $($request.remoteendpoint) by $($request.useragent)"
				if ($request.headers['Metadata'] -eq $true -and $request.url.absolutepath -eq '/metadata/identity/oauth2/token') {
					$request_returnid = $request.headers['x-ms-return-client-request-id']
					$request_requestid = $request.headers['x-ms-client-request-id']
					if ($request_returnid -eq $true) {
						$response.headers['x-ms-client-request-id'] = $request_requestid
					}
					$request_resource = $request.querystring['resource']
					$imds_token = $cache.get($request_resource)
					if ($null -eq $imds_token) {
						write-host -f cyan "$($taskid): getting token for resource $request_resource using Azure CLI"
						$imds_token = az account get-access-token --subscription $account.id --resource $request_resource|convertfrom-json
						$imds_token_expireson = [datetimeoffset]::parseexact($imds_token.expiresOn, 'yyyy-MM-dd HH:mm:ss.ffffff', [globalization.datetimeformatinfo]::invariantinfo)
						$imds_token_cache_expireson = $imds_token_expireson.addminutes(-5)
						write-host -f cyan "$($taskid): got token for resource $request_resource expiring on $($imds_token.expireson) from Azure CLI"
						$cache.add($request_resource, $imds_token, $imds_token_cache_expireson)|out-null
					} else {write-host -f cyan "$($taskid): got token for resource $request_resource expiring $($imds_token.expireson) from cache"}
					$access_token = $imds_token.accesstoken
					$access_token_payload_raw = $access_token.split('.')[1].replace('-','+').replace('_','/')
					$access_token_payload_json = [text.encoding]::ascii.getstring([convert]::frombase64string($access_token_payload_raw.padright($access_token_payload_raw.length+((4-($access_token_payload_raw.length%4))%4),'=')))
					$access_token_payload = $access_token_payload_json|convertfrom-json
					$response_obj = @{
						token_type = $imds_token.tokentype
						resource = $access_token_payload.aud
						not_before = $access_token_payload.nbf
						expires_on = $access_token_payload.exp
						expires_in = $access_token_payload.exp - $access_token_payload.iat
						refresh_token = ''
						access_token = $access_token
					}
					$response_json = $response_obj|convertto-json -compress
					$response_bytes = [text.encoding]::utf8.getbytes($response_json)
					$response.statuscode = 200
					$response.contenttype = 'application/json'
					$response.contentlength64 = $response_bytes.length
					write-host -f cyan "$($taskid): returning token for resource $($response_obj.resource)`n"
					$response.outputstream.write($response_bytes, 0, $response_bytes.length)
				} else {
					write-host -f red "$($taskid): returning 401 since metadata header '$($request.headers['Metadata'])' or path '$($request.url.absolutepath)' did not match`n"
					$response.statuscode = 401
				}
				$response.close()
			} else {
				write-host -f red "$($taskid): error handling connection $($task.exception)`n"
			}
		}
	} finally {
		write-host -f green "Clearing cache of $($cache.getcount()) items"
		$cache.dispose()
		write-host -f green "Stopping IMDS token endpoint on $($listener.prefixes)"
		$listener.stop()
		write-host -f green "Stopped IMDS token endpoint on $($listener.prefixes)"
	}
}

Finally if desired you can cleanup by removing the IP address, although it will be automatically removed on next Windows restart.

# remove the ip address if you want to do so before a system restart
netsh interface ipv4 delete address name=1 address=169.254.169.254 store=active
@JustinGrote
Copy link

Very nice! I made a version that doesn't require admin access, since most tools like func and whatnot look for the MSI_ENDPOINT or IDENTITY_ENDPOINT environment variables.

https://gist.github.com/JustinGrote/2b7ff3c08b38ba459f9079e456e49563

@maskati
Copy link
Author

maskati commented Aug 29, 2024

@JustinGrote nice addition! You could add attribution to the comments 😉

@JustinGrote
Copy link

Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment