From 06e2336b2d0c77a0d08cb937c7ec9b96908e8278 Mon Sep 17 00:00:00 2001 From: Robin Duhan <52245137+RobinDuhan@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:22:05 +0530 Subject: [PATCH] INST-22223 - Updating Azure.md and release changes for 1.4.0 --- AZURE.md | 285 ++++++++++++++++++++++++++++++++++++++++------- helm/Chart.yaml | 2 +- helm/values.yaml | 2 +- 3 files changed, 248 insertions(+), 41 deletions(-) diff --git a/AZURE.md b/AZURE.md index a58a4c7..19a641a 100644 --- a/AZURE.md +++ b/AZURE.md @@ -6,9 +6,17 @@ For **OpenShift on Azure** (including Azure Red Hat OpenShift), see [OpenShift.m ## 📋 Overview -The JFrog Credentials Provider uses Azure managed identities to authenticate with JFrog Artifactory via OpenID Connect (OIDC). This eliminates the need for manual image pull secret management by dynamically retrieving credentials when pulling container images. +The JFrog Credentials Provider authenticates with JFrog Artifactory via OpenID Connect (OIDC), eliminating manual image pull secret management by dynamically retrieving credentials when pulling container images. -### 🔄 How It Works +There are **three** ways to set this up (see [Setup Process](#-setup-process) for the full comparison): + +- **Option A — Nodepool Managed Identity:** Azure AD app registration **+ federated credential**. +- **Option B — Workload Identity (projected SA tokens):** ⭐ **No Azure AD app registration at all** — the simplest and most scalable option; Artifactory trusts the cluster's OIDC issuer and the workload's Kubernetes Service Account directly. +- **Option C — IMDS Direct:** Azure AD app registration but **no federated credential** (uses an app-role assignment instead), to avoid Azure's federated-credential limit. + +> The diagram and "How It Works" below describe **Option A** (federated credentials). Options B and C are simpler — see their dedicated sections ([4B](#step-4b-workload-identity--projected-service-account-tokens-no-app-registration), [4C](#imds-direct-authentication)). + +### 🔄 How It Works (Option A) ```mermaid @@ -77,23 +85,33 @@ helm version ## 🚀 Setup Process -There are **two authentication methods** available for the credential provider: +There are **three authentication methods** available. They differ mainly in **whether they need an Azure AD app registration** and **whether they need a federated identity credential** — the two things that make Azure setups hard to scale. + +| Option | Method | Azure AD app registration? | Federated credential? | Steps | +|--------|--------|:--:|:--:|-------| +| **A** | Nodepool Managed Identity | ✅ Required | ✅ Required | 1 → 2 → 3 → 4A → 5 | +| **B** | **Workload Identity (projected SA tokens)** | ❌ **Not needed** | ❌ **Not needed** | **4B → 5** | +| **C** | IMDS Direct | ✅ Required | ❌ **Not needed** | 1 → 2 → 4C → 5 | + +- **Option A: Nodepool Managed Identity (federated credentials)** — The nodepool's user-assigned managed identity (UAMI) is exchanged for an Azure AD app token via a **federated identity credential**, then exchanged with Artifactory. + > **Choose this when:** You want node-level identity and are not constrained by Azure's federated identity credential limit. -- **Option A: Nodepool Managed Identity** (Steps 1 → 2 → 3 → 4A → 5) — Uses the AKS nodepool's user-assigned managed identity to authenticate via Azure IMDS. - > **Choose this when:** You want a straightforward setup, all nodes in the pool can share the same identity, and you don't need per-workload credential isolation. +- **Option B: Workload Identity (projected service account tokens)** — ⭐ **The simplest, most scalable option, and it requires no Azure AD app registration at all.** The kubelet's projected Kubernetes service account token is sent **directly** to Artifactory, which trusts your cluster's OIDC issuer. There is no Azure AD app, no federated credential, and no `azure.workload.identity/client-id` annotation — each workload is identified purely by its Kubernetes Service Account (`sub`). + > **Choose this when:** You want the least Azure setup, per-service-account isolation, and zero coupling to Azure AD app registrations. -- **Option B: Workload Identity** (Steps 1 → 2 → 3 → 4B → 5) — Uses Kubernetes projected service account tokens. Provides better security isolation as each service account can have its own identity. - > **Choose this when:** You need fine-grained, per-service-account access control, want to follow the zero-trust principle, or your organization requires workload-level identity isolation. +- **Option C: IMDS Direct (no federated credentials)** — The nodepool UAMI fetches an app-scoped access token **directly from Azure IMDS** (using an app-role assignment instead of a federated credential) and sends it straight to Artifactory. + > **Choose this when:** You need a node-level identity but are hitting (or want to avoid) Azure's **federated identity credential limit** — a single app registration supports only a limited number of federated credentials. Option C needs none, so one app registration can serve any number of clusters and nodepools. -The setup process consists of the following steps: +The setup steps are: -1. **Identify Azure Cloud Name** - Determine your Azure cloud environment (optional for `AzureCloud`, required for sovereign clouds) -2. **Azure AD App Registration** - Create an enterprise application in Azure AD -3. **Federated Identity Credentials** - Configure AKS nodepool access to the Azure App -4. **JFrog Artifactory OIDC Configuration** - Choose one of: - - **Step 4A:** Configure using Nodepool Managed Identity - - **Step 4B:** Configure using Workload Identity (Projected Service Account Tokens) -5. **Deploy Credentials Provider** - Deploy the credential provider using Helm +1. **Identify Azure Cloud Name** — Azure cloud environment (Options A and C; optional for `AzureCloud`) +2. **Azure AD App Registration** — Create the app registration (**Options A and C only — Option B needs no app registration**) +3. **Federated Identity Credentials** — Trust the nodepool UAMI (**Option A only**) +4. **JFrog Artifactory OIDC Configuration** — Choose one of: + - **Step 4A:** Nodepool Managed Identity (federated credentials) + - **Step 4B:** Workload Identity — projected SA tokens (**no app registration**) + - **Step 4C:** IMDS Direct (no federated credentials) +5. **Deploy Credentials Provider** — Deploy with Helm --- @@ -473,19 +491,20 @@ curl -X GET "https://$ARTIFACTORY_URL/access/api/v1/oidc/$OIDC_PROVIDER_NAME" \ --- -## Step 4B: Using Projected Service Account Tokens (Workload Identity) +## Step 4B: Workload Identity — Projected Service Account Tokens (No App Registration) -Instead of using the Nodepool's Managed Identity, you can use **Kubernetes Workload Identity**. This allows the Credential Provider to use a specific Kubernetes Service Account to authenticate with Artifactory. This method provides better security isolation as each service account can have its own Azure AD app registration. +> ⭐ **No Azure AD app registration is required for this flow.** You can skip Steps 1, 2, and 3 entirely. The pulling workload is identified purely by its **Kubernetes Service Account**, and Artifactory trusts your **cluster's OIDC issuer** directly. There is no Azure AD app, no federated credential, and no `azure.workload.identity/client-id` annotation. -**Flow Overview:** - -1. The credential provider requests a service account token from Kubernetes with the AKS OIDC issuer audience +This is the simplest and most scalable option. The kubelet projects the pulling pod's Service Account token and the credential provider sends it **straight to Artifactory**, which validates it against your cluster's OIDC issuer and the Service Account subject. -2. The provider exchanges the service account token for an OIDC access token from Azure AD using federated credentials +**Flow Overview:** -3. The provider exchanges the Azure OIDC token with Artifactory, which validates it and returns a short-lived registry access token +1. The kubelet projects a service account token for the pulling pod's Service Account (issued by the cluster's OIDC issuer). +2. The credential provider sends that projected token **directly** to Artifactory — there is no Azure AD or app registration involved. +3. Artifactory validates the token claims (`iss` = cluster OIDC issuer, `sub` = the Service Account) and returns a short-lived registry access token. +4. The kubelet uses the registry token to pull the container image. -4. The kubelet uses the registry token to authenticate and pull the container image +> **â„šī¸ What you do *not* need for this flow:** an Azure AD app registration, a federated identity credential, `azure_app_client_id`, or the `azure.workload.identity/client-id` annotation. The only Service Account annotation required is `JFrogExchange: "true"`. ### Step 4B.1: ✅ Enable OIDC Issuer on AKS @@ -529,15 +548,15 @@ kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply - # Create the service account kubectl create serviceaccount "$SERVICE_ACCOUNT_NAME" -n "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - -# Annotate the service account with your Azure App Client ID and Workload Identity marker +# Annotate the service account with the JFrogExchange marker only. +# No azure.workload.identity/client-id annotation is needed for this flow. kubectl annotate serviceaccount "$SERVICE_ACCOUNT_NAME" \ -n "$NAMESPACE" \ - azure.workload.identity/client-id="$APP_CLIENT_ID" \ JFrogExchange="true" \ --overwrite ``` -> **â„šī¸ Note:** The `JFrogExchange="true"` annotation tells the credential provider to use the projected service account token instead of the nodepool's managed identity. +> **â„šī¸ Note:** The `JFrogExchange="true"` annotation is what tells the credential provider to use the projected service account token instead of the nodepool's managed identity. It is the **only** annotation required for this flow. ### Step 4B.3: 🐸 Update JFrog Artifactory OIDC Configuration @@ -562,7 +581,7 @@ curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc" \ #### Create Identity Mapping for Service Account: -The `sub` (subject) claim must specifically target your Kubernetes Service Account: +The mapping identifies the workload purely by its **Service Account subject** (`sub`) and your **cluster OIDC issuer** (`iss`) — no Azure app or audience is involved: ```bash curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc/aks-workload-identity/identity_mappings" \ @@ -572,7 +591,6 @@ curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc/aks-workload-identity/ \"name\": \"aks-workload-identity-mapping\", \"description\": \"Azure AKS Workload Identity mapping\", \"claims\": { - \"aud\": \"api://AzureADTokenExchange\", \"iss\": \"$SERVICE_ACCOUNT_ISSUER\", \"sub\": \"system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT_NAME}\" }, @@ -586,7 +604,158 @@ curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc/aks-workload-identity/ }" ``` -> **âš ī¸ Important:** The `sub` claim must exactly match the Kubernetes service account format: `system:serviceaccount::` +> **âš ī¸ Important:** The `sub` claim must exactly match the Kubernetes service account format: `system:serviceaccount::`. +> +> **â„šī¸ Note:** The mapping only needs `iss` + `sub`. If you also want to pin the token audience, set the projected token audience via `azure_app_audience` in your Helm values (it becomes the token's `aud`) and add a matching `aud` claim here — but this is optional. + +--- + + + +## Step 4C: 🆕 IMDS Direct Authentication (No Federated Credentials) + +**Option C** uses the AKS nodepool's user-assigned managed identity to request an **app-scoped access token directly from Azure IMDS** and sends that token straight to Artifactory. Unlike Option A, it does **not** perform a client-assertion exchange against the Azure AD token endpoint, so it does **not** require a federated identity credential on the app registration. + +Because Azure limits the number of federated identity credentials per app registration, Options A and B can become a scaling bottleneck across many clusters/nodepools. IMDS Direct removes that limit entirely — a single app registration can serve any number of nodepools. + +**Flow Overview:** + +```mermaid +sequenceDiagram + participant Pod + participant Kubelet + participant Plugin as Credential Provider + participant IMDS as Azure IMDS + participant Artifactory as JFrog Artifactory + + Pod->>Kubelet: Request image pull + Kubelet->>Plugin: Execute plugin (image matches pattern) + Plugin->>IMDS: Request access token
(nodepool client_id, resource = azure_app_uri) + Note over IMDS: Returns an app-scoped access token
for the nodepool managed identity
(no federated credential needed) + IMDS-->>Plugin: Return access token + Plugin->>Artifactory: Exchange access token for registry token + Note over Artifactory: Validates token claims (iss, aud) + Artifactory-->>Plugin: Return short-lived registry token + Plugin-->>Kubelet: Return credentials (username, token) + Kubelet->>Artifactory: Pull image using credentials + Artifactory-->>Kubelet: Image data + Kubelet-->>Pod: Image available +``` + +Instead of a federated credential, the nodepool's managed identity is granted an **app role assignment** on the app's service principal. With **Assignment required = true**, that assignment is what authorizes the managed identity to request an app-scoped token from IMDS. + +### 4C.1: Prepare the App Registration + +Complete **Step 2** to create the app registration and service principal and set `requestedAccessTokenVersion: 2`. Then set the **Application ID URI** (the `resource` the credential provider requests). It defaults to `api://`: + +```bash +# APP_CLIENT_ID and TENANT_ID from Step 2 +APP_ID_URI="api://${APP_CLIENT_ID}" + +az ad app update --id "$APP_CLIENT_ID" --identifier-uris "$APP_ID_URI" +echo "APP_ID_URI=$APP_ID_URI" +``` + +> **â„šī¸ For Option C you do NOT need** the federated credential (Step 3), nor the custom app role / "assign the SP to itself" sub-steps from Step 2. Instead, you assign the nodepool managed identity below. + +### 4C.2: Require assignment and assign the nodepool managed identity + +```bash +# Service principal (object ID) of the app registration +APP_SP_OBJECT_ID=$(az ad sp list --filter "appId eq '$APP_CLIENT_ID'" --query "[0].id" -o tsv) + +# Require explicit assignment so only authorized identities can get a token for this app +az rest --method PATCH \ + --uri "$GRAPH_ENDPOINT/v1.0/servicePrincipals/$APP_SP_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body '{"appRoleAssignmentRequired": true}' + +# Object ID of the nodepool's user-assigned managed identity (see Step 3 for how to find NODEPOOL_IDENTITY_NAME / NODE_RESOURCE_GROUP) +UAMI_OBJECT_ID=$(az identity show \ + --resource-group "$NODE_RESOURCE_GROUP" \ + --name "$NODEPOOL_IDENTITY_NAME" \ + --query "principalId" -o tsv) + +# Assign the managed identity to the app using the implicit "default access" role. +# This role (all-zeros GUID) satisfies appRoleAssignmentRequired without authoring a custom role. +az rest --method POST \ + --uri "$GRAPH_ENDPOINT/v1.0/servicePrincipals/$UAMI_OBJECT_ID/appRoleAssignments" \ + --headers "Content-Type=application/json" \ + --body "{ + \"principalId\": \"$UAMI_OBJECT_ID\", + \"resourceId\": \"$APP_SP_OBJECT_ID\", + \"appRoleId\": \"00000000-0000-0000-0000-000000000000\" + }" +``` + +You also need the nodepool managed identity's **client ID** (`azure_nodepool_client_id`) for the Helm values — retrieve it with the commands in [Step 3 → Get the User-Assigned Managed Identity](#-get-the-user-assigned-managed-identity-nodepool-client-id). + +### 4C.4: 🐸 JFrog Artifactory OIDC Configuration + +Create an OIDC provider and identity mapping pointing at your **Azure AD tenant issuer**: + +```bash +# OIDC provider (Azure AD tenant issuer) +curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ARTIFACTORY_ADMIN_TOKEN" \ + -d "{ + \"name\": \"$OIDC_PROVIDER_NAME\", + \"issuer_url\": \"$AD_ENDPOINT/$TENANT_ID/v2.0\", + \"provider_type\": \"Azure\", + \"token_issuer\": \"$AD_ENDPOINT/$TENANT_ID/v2.0\", + \"azure_app_id\": \"$APP_CLIENT_ID\", + \"audience\": \"$APP_CLIENT_ID\", + \"use_default_proxy\": false, + \"description\": \"OIDC provider for Azure AKS IMDS Direct\" + }" + +# Identity mapping +curl -X POST "https://$ARTIFACTORY_URL/access/api/v1/oidc/$OIDC_PROVIDER_NAME/identity_mappings" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ARTIFACTORY_ADMIN_TOKEN" \ + -d "{ + \"name\": \"$OIDC_PROVIDER_NAME-mapping\", + \"description\": \"Azure IMDS Direct identity mapping\", + \"claims\": { + \"aud\": \"$APP_CLIENT_ID\", + \"iss\": \"$AD_ENDPOINT/$TENANT_ID/v2.0\" + }, + \"token_spec\": { + \"username\": \"$ARTIFACTORY_USER\", + \"scope\": \"applied-permissions/user\", + \"audience\": \"*@*\", + \"expires_in\": 14400 + }, + \"priority\": 1 + }" +``` + +> **â„šī¸ Note:** `claims.aud` is the **bare app client ID** and `claims.iss` is the `v2.0` issuer (as verified in [4C.3](#4c3-optional-verify-the-imds-token-from-a-node)). The `token_spec.audience` (`*@*`) is matched by `jfrog_token_audience` (default `*@*`); only change it if you intentionally narrow the resulting Artifactory token's audience. + +### Helm values + +See [`examples/azure-non-federated-values.yaml`](./examples/azure-non-federated-values.yaml). + +```yaml +providerConfig: + - name: jfrog-credentials-provider + artifactoryUrl: "" + matchImages: + - "" + defaultCacheDuration: 5h + azure: + enabled: true + azure_auth_method: "imds_direct" # required: selects the IMDS Direct flow + azure_app_client_id: "" + azure_nodepool_client_id: "" + jfrog_oidc_provider_name: "" + # azure_app_uri: "api://" # optional, defaults to api:// + # azure_cloud_name: "AzureCloud" # optional, defaults to AzureCloud + +``` + +> **âš ī¸ Mutually exclusive with Workload Identity:** Do not combine `azure_auth_method: imds_direct` with `tokenAttributes.enabled: true` — the chart fails validation. IMDS Direct is a node-identity flow, so it does not use projected service account tokens. --- @@ -600,6 +769,7 @@ Example values files are provided for each authentication method: - [`./examples/azure-values.yaml`](./examples/azure-values.yaml) — Values file for the Nodepool Managed Identity approach (Option A). - [`./examples/azure-projected-sa-values.yaml`](./examples/azure-projected-sa-values.yaml) — Values file for the Workload Identity approach using projected service account tokens (Option B). +- [`./examples/azure-non-federated-values.yaml`](./examples/azure-non-federated-values.yaml) — Values file for the IMDS Direct approach without federated credentials (Option C). Update the relevant file with your configuration values. @@ -617,11 +787,14 @@ echo "jfrog_oidc_provider_name: $OIDC_PROVIDER_NAME" | Configuration Value | Description | Example | |---------------------|-------------|---------| +| `azure_auth_method` | (Optional) Set to `imds_direct` for Option C (IMDS Direct). Omit (empty) for Options A and B. | `imds_direct` | | `azure_cloud_name` | Your Azure Cloud Name (optional, defaults to `AzureCloud`) | `AzureCloud` `AzureChinaCloud` | -| `azure_tenant_id` | Your Azure AD tenant ID | `12345678-1234-1234-1234-123456789012` | -| `azure_app_client_id` | The Azure AD application client ID | `87654321-4321-4321-4321-210987654321` | -| `azure_nodepool_client_id` | Client ID of the user-assigned managed identity attached to the AKS nodepool (also added to the app registration's federated credential) | `11111111-2222-3333-4444-555555555555` | -| `azure_app_audience` | The OIDC audience | `api://AzureADTokenExchange` | +| `azure_tenant_id` | Your Azure AD tenant ID (Option A only) | `12345678-1234-1234-1234-123456789012` | +| `azure_app_client_id` | The Azure AD application client ID (Options A and C; **not used by Option B**) | `87654321-4321-4321-4321-210987654321` | +| `azure_app_uri` | (Optional) Resource URI requested from IMDS for Option C. Defaults to `api://`. | `api://87654321-...` | +| `azure_nodepool_client_id` | Client ID of the user-assigned managed identity attached to the AKS nodepool (also added to the app registration's federated credential for Option A) | `11111111-2222-3333-4444-555555555555` | +| `azure_app_audience` | The OIDC audience (the projected token audience for Option B) | `api://AzureADTokenExchange` | +| `jfrog_token_audience` | (Optional) Audience requested for the resulting Artifactory token during the OIDC exchange. Defaults to `*@*`. Only set if your identity mapping uses a non-wildcard `token_spec.audience`. | `*@*` | | `jfrog_oidc_provider_name` | The name of the OIDC provider in Artifactory | `azure-aks-oidc-provider` | | `artifactory_url` | Your JFrog Artifactory URL | `your-instance.jfrog.io` | @@ -653,7 +826,7 @@ rbac: #### Configuration for Workload Identity (Projected Service Account Tokens) -Use this configuration if you're using **Kubernetes Workload Identity** (Steps 4B): +Use this configuration if you're using **Kubernetes Workload Identity** (Step 4B). **No Azure AD app registration is involved** — there is no `azure_app_client_id`, no tenant ID, and no cloud name: ```yaml providerConfig: @@ -664,21 +837,46 @@ providerConfig: defaultCacheDuration: 5m tokenAttributes: enabled: true # Enable projected token support - serviceAccountTokenAudience: "" + serviceAccountTokenAudience: "" # the projected token's audience (e.g. api://AzureADTokenExchange) + azure: + enabled: true + azure_app_audience: "" # must match serviceAccountTokenAudience above + jfrog_oidc_provider_name: "" + # jfrog_token_audience: "*@*" # Optional; defaults to *@* + +rbac: + create: true + +# Note: You must also create the service account and annotate it with JFrogExchange="true" as described in Step 4B.2 +``` + +#### Configuration for IMDS Direct (No Federated Credentials) + +Use this configuration if you're using **IMDS Direct** (Step 4C): + +```yaml +providerConfig: + - name: jfrog-credentials-provider + artifactoryUrl: "" + matchImages: + - "" + defaultCacheDuration: 5h + # Do NOT set tokenAttributes.enabled: true here — it is mutually exclusive with imds_direct azure: enabled: true + azure_auth_method: "imds_direct" azure_cloud_name: "" # Optional, defaults to AzureCloud azure_app_client_id: "" - azure_app_audience: "" + azure_nodepool_client_id: "" jfrog_oidc_provider_name: "" + # azure_app_uri: "api://" # Optional, defaults to api:// + # jfrog_token_audience: "*@*" # Optional; defaults to *@* rbac: create: true - -# Note: You must also create the service account and annotate it as described in Step 3B.2 ``` -> **â„šī¸ Note:** When using Workload Identity, ensure the service account `jfrog-provider-sa` is annotated with `JFrogExchange="true"` and the Azure App Client ID as shown in Step 4B.2. +> **â„šī¸ Note:** When using Workload Identity, ensure the service account `jfrog-provider-sa` is annotated with `JFrogExchange="true"` (the only annotation required — see Step 4B.2). ### đŸ“Ļ Install with Helm @@ -712,6 +910,15 @@ helm upgrade --install secret-provider jfrog/jfrog-credential-provider \ -f ./examples/azure-projected-sa-values.yaml ``` +**For IMDS Direct (Option C):** + +```bash +helm upgrade --install secret-provider jfrog/jfrog-credential-provider \ + --namespace jfrog \ + --create-namespace \ + -f ./examples/azure-non-federated-values.yaml +``` + --- ## ✅ Verification diff --git a/helm/Chart.yaml b/helm/Chart.yaml index e9d4262..3c09824 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,7 +3,7 @@ name: jfrog-credential-provider description: A Helm chart for JFrog Credential Provider supporting AWS, Azure, and GCP type: application version: 1.4.0 -appVersion: "1.3.0" +appVersion: "1.4.0" keywords: - jfrog - credential-provider diff --git a/helm/values.yaml b/helm/values.yaml index 0c32b12..d4ae43f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -3,7 +3,7 @@ # Download URL for the credential provider binary # The chart will automatically append the architecture suffix (-amd64 or -arm64) -downloadUrl: "https://releases.jfrog.io/artifactory/run/jfrog-credentials-provider/1.3.0/jfrog-credential-provider-linux" +downloadUrl: "https://releases.jfrog.io/artifactory/run/jfrog-credentials-provider/1.4.0/jfrog-credential-provider-linux" # Kubernetes distribution (optional). Omit for EKS, AKS, and GKE (default behavior). # Set to "openshift" for OpenShift on AWS or Azure — see OpenShift.md.