forked from rancher/os-services
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
azure-identity: | ||
image: ${REGISTRY_DOMAIN}/burmilla/os-azureidentity:v1 | ||
environment: | ||
- AZIDENTITY_* | ||
labels: | ||
io.rancher.os.scope: system | ||
io.rancher.os.before: docker | ||
net: host | ||
restart: always |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
azidentity | ||
*.exe |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/sdw/azidentity | ||
|
||
go 1.21.5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ services: | |
- modem-manager | ||
- waagent | ||
- docker-compose | ||
- azure-identity | ||
engines: | ||
- docker-24.0.9 | ||
- docker-25.0.3 |