Skip to content

Commit

Permalink
plugins/rest/azure: Support managed identity for App Servivce / Conta…
Browse files Browse the repository at this point in the history
…iner Apps

IDENTITY_ENDPOINT and IDENTITY_HEADER envirnnment variables are
provided on Azure App Service for getting the token.
We can detect these variables and switch the endpoint
and header value from IMDS.

Fixes: open-policy-agent#7085
Signed-off-by: Hitoshi Kamezaki <[email protected]>
  • Loading branch information
apc-kamezaki committed Oct 8, 2024
1 parent 69cd388 commit 08a7fe1
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 16 deletions.
8 changes: 6 additions & 2 deletions docs/content/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,15 +535,19 @@ 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. |
| `services[_].credentials.azure_managed_identity.mi_res_id` | `string` | No | Optional Azure Resource 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.use_app_service_msi` | `bool` | No | If you force to use `Azure Instance Metadata Service`, set false this value and configure endpoint explicitly. Default: set automatically when you use managed identity on Azure App Servic or Container Apps. |

The following is an example of how to use an [Azure storage
account](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-overview)
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")
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

0 comments on commit 08a7fe1

Please sign in to comment.