Skip to content

Commit d6c5749

Browse files
committed
self-hosted-runner: switch to federated credentials
Instead of storing stealable credentials in repository secrets, let's create a managed identity instead and use federated credentials via GitHub Actions' support for OpenID Connect. This binds the authorization to GitHub workflows running in a specific repository, and stealing the information won't enable an attacker to get authorized to use the Azure subscription. Note that this change _does_ require Client ID, Tenant ID and Subscription ID to be stored separately as repository secrets (although they do not _technically_ need to be kept secret, security is a game of layers, so why give away this information?). Also note that for some strange reason, the `contents: read` permission seems to be lost when introducing a `permissions:` section. Therefore we have to add it back explicitly, otherwise `actions/checkout` will fail in a private repository. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 11e20e0 commit d6c5749

File tree

4 files changed

+94
-50
lines changed

4 files changed

+94
-50
lines changed
Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,31 @@
11
name: Azure Login
22
description: Logs into Azure using a service principal
33
inputs:
4-
credentials:
5-
description: Your credentials in JSON format
4+
client-id:
5+
description: The Client ID of the Azure Managed Identity
6+
required: true
7+
tenant-id:
8+
description: The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which the Identity lives)
69
required: true
710

811
runs:
912
using: "composite"
1013
steps:
11-
- name: Process Azure credentials
14+
- name: Obtain OpenID Connect token
1215
uses: actions/github-script@v7
13-
env:
14-
AZURE_CREDENTIALS: ${{ inputs.credentials }}
15-
with:
16-
script: |
17-
if (!process.env.AZURE_CREDENTIALS) {
18-
core.setFailed('The AZURE_CREDENTIALS secret is required.')
19-
process.exit(1)
20-
}
21-
22-
const azureCredentials = JSON.parse(process.env.AZURE_CREDENTIALS)
23-
const {clientId, clientSecret, tenantId, subscriptionId} = azureCredentials
24-
25-
core.setSecret(clientId)
26-
core.exportVariable('AZURE_CLIENT_ID', clientId)
27-
28-
core.setSecret(clientSecret)
29-
core.exportVariable('AZURE_CLIENT_SECRET', clientSecret)
30-
31-
core.setSecret(tenantId)
32-
core.exportVariable('AZURE_TENANT_ID', tenantId)
16+
id: token
17+
with:
18+
script: |
19+
const token = await core.getIDToken('api://AzureADTokenExchange')
20+
core.setSecret(token)
21+
return token
3322
34-
core.setSecret(subscriptionId)
35-
core.exportVariable('AZURE_SUBSCRIPTION_ID', subscriptionId)
36-
3723
- name: Azure Login
3824
shell: bash
3925
run: |
4026
echo "Logging into Azure..."
41-
az login --service-principal -u ${{ env.AZURE_CLIENT_ID }} -p ${{ env.AZURE_CLIENT_SECRET }} --tenant ${{ env.AZURE_TENANT_ID }}
42-
echo "Setting subscription..."
43-
az account set --subscription ${{ env.AZURE_SUBSCRIPTION_ID }} --output none
27+
az login --service-principal \
28+
--username ${{ inputs.client-id }} \
29+
--tenant ${{ inputs.tenant-id }} \
30+
--federated-token ${{ steps.token.outputs.result }} \
31+
--output none

.github/workflows/cleanup-self-hosted-runners.yml

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,28 @@ on:
77
- cron: "0 */6 * * *"
88
workflow_dispatch:
99

10+
permissions:
11+
id-token: write # required for Azure login via OIDC
12+
1013
# The following secrets are required for this workflow to run:
11-
# AZURE_CREDENTIALS - Credentials for the Azure CLI. It's recommended to set up a resource
12-
# group specifically for self-hosted Actions Runners.
13-
# az ad sp create-for-rbac --name "{YOUR_DESCRIPTIVE_NAME_HERE}" --role contributor \
14-
# --scopes /subscriptions/{SUBSCRIPTION_ID_HERE}/resourceGroups/{RESOURCE_GROUP_HERE} \
15-
# --sdk-auth
14+
# AZURE_CLIENT_ID - The Client ID of an Azure Managed Identity. It is recommended to set up a resource
15+
# group specifically for self-hosted Actions Runners, and to add a federated identity
16+
# to authenticate as the currently-running GitHub workflow.
17+
# az identity create --name <managed-identity-name> -g <resource-group>
18+
# az identity federated-credential create \
19+
# --identity-name <managed-identity-name> \
20+
# --resource-group <resource-group> \
21+
# --name github-workflow \
22+
# --issuer https://token.actions.githubusercontent.com \
23+
# --subject repo:git-for-windows/git-for-windows-automation:ref:refs/heads/main \
24+
# --audiences api://AzureADTokenExchange
25+
# MSYS_NO_PATHCONV=1 \
26+
# az role assignment create \
27+
# --assignee <client-id-of-managed-identity> \
28+
# --scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>' \
29+
# --role 'Contributor'
30+
# AZURE_TENANT_ID - The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which
31+
# the Identity lives)
1632
# AZURE_RESOURCE_GROUP - Resource group to find the runner(s) in. It's recommended to set up a resource
1733
# group specifically for self-hosted Actions Runners.
1834
jobs:
@@ -24,8 +40,8 @@ jobs:
2440
- name: Azure Login
2541
uses: ./.github/workflows/azure-login
2642
with:
27-
credentials: ${{ secrets.AZURE_CREDENTIALS }}
28-
43+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
44+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
2945
- name: Discover VMs to delete
3046
env:
3147
GH_APP_ID: ${{ secrets.GH_APP_ID }}

.github/workflows/create-azure-self-hosted-runners.yml

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,29 @@ env:
5353
AZURE_VM_REGION: westus2
5454
AZURE_VM_IMAGE: win11-24h2-ent
5555

56+
permissions:
57+
id-token: write # required for Azure login via OIDC
58+
contents: read
59+
5660
# The following secrets are required for this workflow to run:
57-
# AZURE_CREDENTIALS - Credentials for the Azure CLI. It's recommended to set up a resource
58-
# group specifically for self-hosted Actions Runners.
59-
# az ad sp create-for-rbac --name "{YOUR_DESCRIPTIVE_NAME_HERE}" --role contributor \
60-
# --scopes /subscriptions/{SUBSCRIPTION_ID_HERE}/resourceGroups/{RESOURCE_GROUP_HERE} \
61-
# --sdk-auth
61+
# AZURE_CLIENT_ID - The Client ID of an Azure Managed Identity. It is recommended to set up a resource
62+
# group specifically for self-hosted Actions Runners, and to add a federated identity
63+
# to authenticate as the currently-running GitHub workflow.
64+
# az identity create --name <managed-identity-name> -g <resource-group>
65+
# az identity federated-credential create \
66+
# --identity-name <managed-identity-name> \
67+
# --resource-group <resource-group> \
68+
# --name github-workflow \
69+
# --issuer https://token.actions.githubusercontent.com \
70+
# --subject repo:git-for-windows/git-for-windows-automation:ref:refs/heads/main \
71+
# --audiences api://AzureADTokenExchange
72+
# MSYS_NO_PATHCONV=1 \
73+
# az role assignment create \
74+
# --assignee <client-id-of-managed-identity> \
75+
# --scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>' \
76+
# --role 'Contributor'
77+
# AZURE_TENANT_ID - The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which
78+
# the Identity lives)
6279
# AZURE_RESOURCE_GROUP - Resource group to create the runner(s) in
6380
# AZURE_VM_USERNAME - Username of the VM so you can RDP into it
6481
# AZURE_VM_PASSWORD - Password of the VM so you can RDP into it
@@ -163,8 +180,9 @@ jobs:
163180
- name: Azure Login
164181
uses: ./.github/workflows/azure-login
165182
with:
166-
credentials: ${{ secrets.AZURE_CREDENTIALS }}
167-
183+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
184+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
185+
168186
- uses: azure/arm-deploy@v2
169187
id: deploy-arm-template
170188
with:

.github/workflows/delete-self-hosted-runner.yml

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,43 @@ on:
1212
env:
1313
ACTIONS_RUNNER_NAME: ${{ github.event.inputs.runner_name }}
1414

15+
permissions:
16+
id-token: write # required for Azure login via OIDC
17+
1518
# The following secrets are required for this workflow to run:
16-
# AZURE_CREDENTIALS - Credentials for the Azure CLI. It's recommended to set up a resource
17-
# group specifically for self-hosted Actions Runners.
18-
# az ad sp create-for-rbac --name "{YOUR_DESCRIPTIVE_NAME_HERE}" --role contributor \
19-
# --scopes /subscriptions/{SUBSCRIPTION_ID_HERE}/resourceGroups/{RESOURCE_GROUP_HERE} \
20-
# --sdk-auth
21-
# AZURE_RESOURCE_GROUP - Resource group to create the runner(s) in
19+
# AZURE_CLIENT_ID - The Client ID of an Azure Managed Identity. It is recommended to set up a resource
20+
# group specifically for self-hosted Actions Runners, and to add a federated identity
21+
# to authenticate as the currently-running GitHub workflow.
22+
# az identity create --name <managed-identity-name> -g <resource-group>
23+
# az identity federated-credential create \
24+
# --identity-name <managed-identity-name> \
25+
# --resource-group <resource-group> \
26+
# --name github-workflow \
27+
# --issuer https://token.actions.githubusercontent.com \
28+
# --subject repo:git-for-windows/git-for-windows-automation:ref:refs/heads/main \
29+
# --audiences api://AzureADTokenExchange
30+
# MSYS_NO_PATHCONV=1 \
31+
# az role assignment create \
32+
# --assignee <client-id-of-managed-identity> \
33+
# --scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>' \
34+
# --role 'Contributor'
35+
# AZURE_TENANT_ID - The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which
36+
# the Identity lives)
37+
# AZURE_SUBSCRIPTION_ID - The Subscription ID with which the Azure Managed Identity is associated
38+
# (technically, this is not necessary for `az login --service-principal` with a
39+
# managed identity, but `Azure/login` requires it anyway)
40+
# AZURE_RESOURCE_GROUP - Resource group to find the runner in. It's recommended to set up a resource
41+
# group specifically for self-hosted Actions Runners.
2242
jobs:
2343
delete-runner:
2444
runs-on: ubuntu-latest
2545
steps:
2646
- name: Azure Login
2747
uses: azure/login@v2
2848
with:
29-
creds: ${{ secrets.AZURE_CREDENTIALS }}
49+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
50+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
51+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
3052
- name: Delete VM '${{ env.ACTIONS_RUNNER_NAME }}'
3153
uses: azure/CLI@v2
3254
with:

0 commit comments

Comments
 (0)