Skip to content
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

Support managed identity for Azure App Service/Azure Container Apps #7086

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/content/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,14 @@ When the given resource (the object in the GCS bucket) contains slashes (/) or o
OPA will authenticate with an [Azure managed identities](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) token.
The [token request](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http)
can be configured via the plugin to customize the base URL, API version, and resource. Specific managed identity IDs can be optionally provided as well.
(The token request for [Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#connect-to-azure-services-in-app-code) or
[Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity?tabs=bicep%2Chttp#connect-to-azure-services-in-app-code) is similar to above interface,
but the endpoint and the header are different. Please see the individual documents for more details.)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.azure_managed_identity.endpoint` | `string` | No | Request endpoint. (default: `http://169.254.169.254/metadata/identity/oauth2/token`, the Azure Instance Metadata Service endpoint (recommended))|
| `services[_].credentials.azure_managed_identity.api_version` | `string` | No | API version to use. (default: `2018-02-01`, the minimum version) |
| `services[_].credentials.azure_managed_identity.endpoint` | `string` | No | Request endpoint. (Detect endpoint from IDENTITY_ENDPOINT environment variable when you use managed identity on Azure App Service or Container Apps. Otherwise set default: `http://169.254.169.254/metadata/identity/oauth2/token`, the Azure Instance Metadata Service endpoint (recommended))|
| `services[_].credentials.azure_managed_identity.api_version` | `string` | No | API version to use. (default: `2019-08-01` when you use `IDENTITY_ENDPONT` endpoint, otherwise `2018-02-01`, the minimum version) |
| `services[_].credentials.azure_managed_identity.resource` | `string` | No | App ID URI of the target resource. (default: `https://storage.azure.com/`) |
| `services[_].credentials.azure_managed_identity.object_id` | `string` | No | Optional object ID of the managed identity you would like the token for. Required, if your VM has multiple user-assigned managed identities. |
| `services[_].credentials.azure_managed_identity.client_id` | `string` | No | Optional client ID of the managed identity you would like the token for. Required, if your VM has multiple user-assigned managed identities. |
Expand Down
49 changes: 36 additions & 13 deletions plugins/rest/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"io"
"net/http"
"net/url"
"os"
"time"
)

var (
azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
defaultAPIVersion = "2018-02-01"
defaultResource = "https://storage.azure.com/"
timeout = 5 * time.Second
azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
defaultAPIVersion = "2018-02-01"
defaultResource = "https://storage.azure.com/"
timeout = 5 * time.Second
defaultAPIVersionForAppServiceMsi = "2019-08-01"
)

// azureManagedIdentitiesToken holds a token for managed identities for Azure resources
Expand All @@ -41,12 +43,13 @@ func (e *azureManagedIdentitiesError) Error() string {

// azureManagedIdentitiesAuthPlugin uses an azureManagedIdentitiesToken.AccessToken for bearer authorization
type azureManagedIdentitiesAuthPlugin struct {
Endpoint string `json:"endpoint"`
APIVersion string `json:"api_version"`
Resource string `json:"resource"`
ObjectID string `json:"object_id"`
ClientID string `json:"client_id"`
MiResID string `json:"mi_res_id"`
Endpoint string `json:"endpoint"`
APIVersion string `json:"api_version"`
Resource string `json:"resource"`
ObjectID string `json:"object_id"`
ClientID string `json:"client_id"`
MiResID string `json:"mi_res_id"`
UseAppServiceMsi bool `json:"use_app_service_msi,omitempty"`
}

func (ap *azureManagedIdentitiesAuthPlugin) NewClient(c Config) (*http.Client, error) {
Expand All @@ -55,15 +58,25 @@ func (ap *azureManagedIdentitiesAuthPlugin) NewClient(c Config) (*http.Client, e
}

if ap.Endpoint == "" {
ap.Endpoint = azureIMDSEndpoint
identityEndpoint := os.Getenv("IDENTITY_ENDPOINT")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any docs or links we can add here that point to the ordering used here? I mean if IDENTITY_ENDPOINT is set, does that determine the flow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if identityEndpoint != "" {
ap.UseAppServiceMsi = true
ap.Endpoint = identityEndpoint
} else {
ap.Endpoint = azureIMDSEndpoint
}
}

if ap.Resource == "" {
ap.Resource = defaultResource
}

if ap.APIVersion == "" {
ap.APIVersion = defaultAPIVersion
if ap.UseAppServiceMsi {
ap.APIVersion = defaultAPIVersionForAppServiceMsi
} else {
ap.APIVersion = defaultAPIVersion
}
}

t, err := DefaultTLSConfig(c)
Expand All @@ -78,6 +91,7 @@ func (ap *azureManagedIdentitiesAuthPlugin) Prepare(req *http.Request) error {
token, err := azureManagedIdentitiesTokenRequest(
ap.Endpoint, ap.APIVersion, ap.Resource,
ap.ObjectID, ap.ClientID, ap.MiResID,
ap.UseAppServiceMsi,
)
if err != nil {
return err
Expand All @@ -90,6 +104,7 @@ func (ap *azureManagedIdentitiesAuthPlugin) Prepare(req *http.Request) error {
// azureManagedIdentitiesTokenRequest fetches an azureManagedIdentitiesToken
func azureManagedIdentitiesTokenRequest(
endpoint, apiVersion, resource, objectID, clientID, miResID string,
useAppServiceMsi bool,
) (azureManagedIdentitiesToken, error) {
var token azureManagedIdentitiesToken
e := buildAzureManagedIdentitiesRequestPath(endpoint, apiVersion, resource, objectID, clientID, miResID)
Expand All @@ -98,7 +113,15 @@ func azureManagedIdentitiesTokenRequest(
if err != nil {
return token, err
}
request.Header.Add("Metadata", "true")
if useAppServiceMsi {
identityHeader := os.Getenv("IDENTITY_HEADER")
if identityHeader == "" {
return token, errors.New("azure managed identities auth: IDENTITY_HEADER env var not found")
}
request.Header.Add("x-identity-header", identityHeader)
} else {
request.Header.Add("Metadata", "true")
}

httpClient := http.Client{Timeout: timeout}
response, err := httpClient.Do(request)
Expand Down
59 changes: 58 additions & 1 deletion plugins/rest/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ func assertParamsEqual(t *testing.T, expected url.Values, actual url.Values, lab
t.Errorf("%s: expected %s, got %s", label, expected.Encode(), actual.Encode())
}
}

func TestAzureManagedIdentitiesAuthPlugin_NewClient(t *testing.T) {
tests := []struct {
label string
Expand Down Expand Up @@ -79,6 +78,64 @@ func TestAzureManagedIdentitiesAuthPlugin_NewClient(t *testing.T) {
}
}

func TestAzureManagedIdentitiesAuthPluginForAppService_NewClient(t *testing.T) {
tests := []struct {
label string
endpoint string
apiVersion string
resource string
objectID string
clientID string
miResID string
}{
{
"test all defaults",
"", "", "", "", "", "",
},
{
"test no defaults",
"some_endpoint", "some_version", "some_resource", "some_oid", "some_cid", "some_miresid",
},
}

nonEmptyString := func(value string, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}

defaultIdentityEndpoint := "http://localhost:42356/msi/token"
defaultIdentityHeader := "IdentityHeader"
t.Setenv("IDENTITY_ENDPOINT", defaultIdentityEndpoint)
t.Setenv("IDENTITY_HEADER", defaultIdentityHeader)

for _, tt := range tests {
config := generateConfigString(tt.endpoint, tt.apiVersion, tt.resource, tt.objectID, tt.clientID, tt.miResID)

client, err := New([]byte(config), map[string]*keys.Config{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

ap := client.config.Credentials.AzureManagedIdentity
_, err = ap.NewClient(client.config)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

// We test that default values are set correctly in the azureManagedIdentitiesAuthPlugin
// Note that there is significant overlap between TestAzureManagedIdentitiesAuthPlugin_NewClient and TestAzureManagedIdentitiesAuthPlugin
// This is because the latter cannot test default endpoint setting, which we do here
assertStringsEqual(t, nonEmptyString(tt.endpoint, defaultIdentityEndpoint), ap.Endpoint, tt.label)
assertStringsEqual(t, nonEmptyString(tt.apiVersion, defaultAPIVersionForAppServiceMsi), ap.APIVersion, tt.label)
assertStringsEqual(t, nonEmptyString(tt.resource, defaultResource), ap.Resource, tt.label)
assertStringsEqual(t, tt.objectID, ap.ObjectID, tt.label)
assertStringsEqual(t, tt.clientID, ap.ClientID, tt.label)
assertStringsEqual(t, tt.miResID, ap.MiResID, tt.label)
}
}

func TestAzureManagedIdentitiesAuthPlugin(t *testing.T) {
tests := []struct {
label string
Expand Down