Skip to content

Commit

Permalink
Add Azure Identity service
Browse files Browse the repository at this point in the history
  • Loading branch information
olljanat committed Mar 8, 2024
1 parent 815061d commit 57ee0ad
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 0 deletions.
9 changes: 9 additions & 0 deletions a/azure-identity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
azure-identity:
image: ${REGISTRY_DOMAIN}/burmilla/os-azure-identity:v1
environment:
- AZIDENTITY_*
labels:
io.rancher.os.scope: system
io.rancher.os.before: docker
net: host
restart: always
2 changes: 2 additions & 0 deletions images/20-azureidentity/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
azidentity
*.exe
12 changes: 12 additions & 0 deletions images/20-azureidentity/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# First stage: Build static binary
FROM golang:1.21-alpine as builder
RUN apk add -U --no-cache ca-certificates
WORKDIR /go/src/azidentity
COPY . .
RUN CGO_ENABLED=0 go build -o azidentity

# Second stage: setup the runtime container
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/azidentity/azidentity .
CMD ["./azidentity"]
81 changes: 81 additions & 0 deletions images/20-azureidentity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Unofficial Azure Managed Identity service
This is service mimics Azure Managed Identity service for non-Azure workloads and makes hybrid configurations simpler.
Client can simply call function `DefaultAzureCredential()` for when they want authenticate with Azure services, just like in Azure.

It was tested with Azure Key Vault but should works with other services too.

# Deployment
## Azure configuration
Create service principal with command `az ad sp create-for-rbac -n burmilla --years 10`

Configure these environment variables based on output of above command with cloud-init configuration like this:
```yaml
rancher:
environment:
AZIDENTITY_TENANTID: 00000000-0000-0000-0000-000000000000
AZIDENTITY_CLIENTID: 11111111-1111-1111-1111-111111111111
AZIDENTITY_SECRET: SecretValue
```

## Network configuration
Because Azure clients expect to find identity service from http://169.254.169.254 we need configure our installation to listening that IP address. It can be done with following cloud-init configurations.
### DHCP
```yaml
rancher:
network:
interfaces:
eth0:
dhcp: true
post_up:
- ip address add 169.254.169.254/32 dev eth0
```

## Static IP
```yaml
rancher:
network:
interfaces:
eth0:
addresses:
- 10.10.10.100/24
- 169.254.169.254/32
```

# Technical details
Implemented by catching Azure Key Vault identity requests with debug proxy.
That can be done by setting HTTP_PROXY and HTTPS_PROXY environment variables **but** you need build custom version which does not contain [this](https://github.com/Azure/azure-sdk-for-net/commit/be063672ae84cf79d18072fdae7a3e362b8d8be7) other why you cannot catch those request.

Sources:
* https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-managed-identities-work-vm#system-assigned-managed-identity
* https://github.com/Azure/azure-sdk-for-net/tree/80c332520a63dad418d6e49ddd139858483b852b/sdk/identity/Azure.Identity#defaultazurecredential
* https://github.com/Azure/azure-sdk-for-net/blob/80c332520a63dad418d6e49ddd139858483b852b/sdk/mgmtcommon/AppAuthentication/Azure.Services.AppAuthentication/TokenProviders/MsiAccessTokenProvider.cs#L78-L81

## Request send by official client:
```bash
GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net HTTP/1.1
Metadata: true
x-ms-client-request-id: 62152641-2531-4057-85eb-a0200fd885b2
x-ms-return-client-request-id: true
User-Agent: azsdk-net-Identity/1.11.0-alpha.20240307.1 (.NET Framework 4.8.4645.0; Microsoft Windows 10.0.20348 )
Host: 169.254.169.254
```

## Azure response:
```bash
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: IMDS/150.870.65.1103
Date: Thu, 07 Mar 2024 11:07:59 GMT
Content-Length: 1669

{
"access_token": "<access token>",
"client_id": "0d46c104-a85f-49e2-9c96-ae58c6fa927b",
"expires_in": "86317",
"expires_on": "1709895997",
"ext_expires_in": "86399",
"not_before": "1709809297",
"resource": "https://vault.azure.net",
"token_type": "Bearer"
}
```
3 changes: 3 additions & 0 deletions images/20-azureidentity/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/sdw/azidentity

go 1.21.5
139 changes: 139 additions & 0 deletions images/20-azureidentity/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strconv"
"time"
)

var (
tenantId string
clientId string
clientSecret string
)

type tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
TokenType string `json:"token_type"`
ErrorDescription string `json:"error_description"`
}

type authResponse struct {
AccessToken string `json:"access_token"`
ClientID string `json:"client_id"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
ExtExpiresIn string `json:"ext_expires_in"`
NotBefore string `json:"not_before"`
Resource string `json:"resource"`
TokenType string `json:"token_type"`
}

func authHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/metadata/identity/oauth2/token" {
http.NotFound(w, r)
return
}

queryParams := r.URL.Query()
resource := queryParams.Get("resource")
if resource == "" {
http.Error(w, "Missing resource parameter", http.StatusBadRequest)
return
}
clientRequestID := r.Header.Get("x-ms-client-request-id")
log.Printf("Client: %s, Client Request ID: %s, Requested Resource: %s\n", r.RemoteAddr, clientRequestID, resource)
scope := resource + "/.default"

currentTime := time.Now()
tokenResponseJson, err := fetchAzureToken(tenantId, clientId, clientSecret, scope)
if err != nil {
log.Printf("Error fetching Azure token: %v", err)
http.Error(w, "Failed to fetch Azure token", http.StatusInternalServerError)
return
}

var azureTokenResponse tokenResponse
if err := json.Unmarshal([]byte(tokenResponseJson), &azureTokenResponse); err != nil {
log.Printf("Error unmarshaling Azure token response: %v", err)
http.Error(w, "Error processing token response", http.StatusInternalServerError)
return
}

if azureTokenResponse.ErrorDescription != "" {
log.Printf("Failed to token from Azure: %v", azureTokenResponse.ErrorDescription)
http.Error(w, "Failed to token from Azure", http.StatusInternalServerError)
return
}

expiresOn := currentTime.Add(time.Second * time.Duration(azureTokenResponse.ExpiresIn)).Unix()
notBefore := currentTime.Unix()

clientResponse := authResponse{
AccessToken: azureTokenResponse.AccessToken,
ExpiresIn: strconv.Itoa(azureTokenResponse.ExpiresIn),
ExpiresOn: strconv.FormatInt(expiresOn, 10),
ExtExpiresIn: strconv.Itoa(azureTokenResponse.ExtExpiresIn),
NotBefore: strconv.FormatInt(notBefore, 10),
Resource: resource,
TokenType: azureTokenResponse.TokenType,
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(clientResponse)
}

func fetchAzureToken(tenantID, clientID, clientSecret, scope string) (string, error) {
tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)

data := url.Values{}
data.Set("client_id", clientID)
data.Set("client_secret", clientSecret)
data.Set("scope", scope)
data.Set("grant_type", "client_credentials")

req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(data.Encode()))
if err != nil {
return "", err
}

req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}

return string(body), nil
}

func main() {
tenantId = os.Getenv("AZIDENTITY_TENANTID")
clientId = os.Getenv("AZIDENTITY_CLIENTID")
clientSecret = os.Getenv("AZIDENTITY_SECRET")

if tenantId == "" || clientId == "" || clientSecret == "" {
log.Fatal("Mandatory environment variables not defined")
}
http.HandleFunc("/metadata/identity/oauth2/token", authHandler)
log.Println("Listening requests on http://169.254.169.254")
if err := http.ListenAndServe("169.254.169.254:80", nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
1 change: 1 addition & 0 deletions index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- modem-manager
- waagent
- docker-compose
- azure-identity
engines:
- docker-24.0.9
- docker-25.0.3

0 comments on commit 57ee0ad

Please sign in to comment.