After my previous fiddling with Azure Container Apps I decided to seeked out how to achive access a private azure network from a GitHub-hosted runner.
How it works
When a workflow is triggered, GitHub creates a runner service and deploys a network interface card (NIC) in the customers private azure network. Once the nic is created an attached, the job is picked up by the runner and started. The runner logs are sent back to GitHub Actions while the runner has access to any resource in the vNet.
Who can use this feature
Azure private networking for GitHub-hosted runners requires an organization with the GitHub Team plan. Creation of GitHub-hosted runners is not allowed on trail plans however, so here I’m upgrading for a month.



Preparation
Before GitHub can be configured access into your vNet there is some prereqs:
- The resource provider
GutHub.Network
must be registered on the subscription with the vNet. - Find the database id for your GitHub organization.
Resource Provider
Set-AzContext -Subscription 'landing-zone-demo-001'
Register-AzResourceProvider -ProviderNamespace 'GitHub.Network'

Find GitHub organization databaseId
To find the databaseId you need a token with minimum read:org
permissions.
Once the PAT is ready, use it to query for your databaseId.
$OrganizationName = 'eskill...'
$BearerToken = '<REDACTED>'
$splat = @{
Uri = 'https://api.github.com/graphql'
Method = 'POST'
Authentication = 'OAuth'
Token = ConvertTo-SecureString -AsPlainText -Force -String $BearerToken
Body = @{
"query" = 'query($login: String!) { organization (login: $login) { login databaseId } }'
"variables" = @{ "login" = $OrganizationName }
} | ConvertTo-Json
}
(Invoke-RestMethod @splat).data.organization
login databaseId
----- ----------
eskill... 123456789
Deploy Azure resources
Now is the time to deploy the azure resources needed. The full example on GitHub deploys all additional resources such as the vNet, subnet, storage account and uami. The following code is a minimal example for the githubNetworkSettings
and nsg
.
param parLocation string
param parNetworkSettingsName string
param parSubnetId string
param parGitHubDatabaseId string
param nsgName string = 'actions_NSG'
resource githubNetworkSettings 'GitHub.Network/networkSettings@2024-04-02' = {
name: parNetworkSettingsName
location: parLocation
properties: {
subnetId: parSubnetId
businessId: parGitHubDatabaseId
}
}
resource actions_NSG 'Microsoft.Network/networkSecurityGroups@2017-06-01' = {
name: nsgName
location: location
properties: {
securityRules: [
{
name: 'AllowVnetOutBoundOverwrite'
properties: {
protocol: 'TCP'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: '*'
destinationAddressPrefix: 'VirtualNetwork'
access: 'Allow'
priority: 200
direction: 'Outbound'
destinationAddressPrefixes: []
}
}
{
name: 'AllowOutBoundActions'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
access: 'Allow'
priority: 210
direction: 'Outbound'
destinationAddressPrefixes: [
'4.175.114.51/32'
...
'20.84.218.150/32'
]
}
}
{
name: 'AllowOutBoundGitHub'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
access: 'Allow'
priority: 220
direction: 'Outbound'
destinationAddressPrefixes: [
'140.82.112.0/20'
...
'4.208.26.200/32'
]
}
}
{
name: 'AllowStorageOutbound'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: 'Storage'
access: 'Allow'
priority: 230
direction: 'Outbound'
destinationAddressPrefixes: []
}
}
{
name: 'DenyInternetOutBoundOverwrite'
properties: {
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: 'Internet'
access: 'Deny'
priority: 1000
direction: 'Outbound'
}
}
]
}
}
Deploy the bicep template to a subscription, here using powershell.
$splat = @{
Name = -join ('github-nics-{0}' -f (Get-Date -Format 'yyyyMMddTHHMMssffffZ'))[0..63]
Location = 'norwayeast'
TemplateFile = 'src/bicep/main.bicep'
TemplateParameterFile = 'main.bicepparam'
Verbose = $true
}
Select-AzSubscription -Subscription 'landing-zone-demo-001'
New-AzSubscriptionDeployment @splat
In the azure portal you can check the box for Show hidden types
to reveal the networksettings. The resource will have your GitHub databaseId as a property and tag.


Create network configuration
Back to GitHub, Azure private networking for GitHub-hosted runners is configurable at the organization level for organization owners. Create a new network configuration and inside it add an Azure Virtual Network. This prompts for details about the azure resource.




Create runner group
Create a runner group for the new runner, assign the network configuration to the group.

Create GitHub-hosted runner
Create a new GitHub-hosted runner and place it in the newly created runner group, as this group is assigned the network configuration.




Workflow in progress
Now with the setup complete, create a GitHub Action workflow upload a file to a private storage account and trigger it manually. Full example here. The important part is to set runs-on:
to be the runner group!
In GitHub you can get an overview of the runner an its jobs, and in the Azure portal you can see new NICs showing up in the resource group.
1env:
2 PRIVATE_STORAGE_ACCOUNT_NAME: ${{ vars.PRIVATE_STORAGE_ACCOUNT_NAME }}
3 PRIVATE_STORAGE_ACCOUNT_CONTAINER_NAME: ${{ vars.PRIVATE_STORAGE_ACCOUNT_CONTAINER_NAME }}
4
5jobs:
6 put-file-in-private-storage:
7 runs-on: [az-vnet-enabled]
8
9 steps:
10 - name: Install pwsh modules
11 shell: pwsh
12 run: 'Install-Module -Name Az.Storage -RequiredVersion 6.1.3 -Force'
13
14 - name: Azure Login
15 uses: azure/login@v2
16 with:
17 client-id: ${{ vars.AZURE_CLIENT_ID }}
18 tenant-id: ${{ secrets.AZURE_TENANT_ID }}
19 subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
20 enable-AzPSSession: true
21
22 - name: Put blob in container
23 shell: pwsh
24 run: |
25 $fileName = "testdata-{0}.log" -f (Get-Date).ToString("yyyyMMdd-HH-mm-ss")
26 Set-Content -Path $fileName -Value "example content"
27 $stContext = New-AzStorageContext -StorageAccountName $env:PRIVATE_STORAGE_ACCOUNT_NAME
28 $splat = @{
29 File = $fileName
30 Container = $env:PRIVATE_STORAGE_ACCOUNT_CONTAINER_NAME
31 Context = $stContext
32 }
33 Set-AzStorageBlobContent @splat


Results
Once the storage account is configured to allow traffic from the runner-subnet, the workflow successfully creates a blob!

Logs
On the logs for the resource group with the runner vNet, there will be events initiated by GitHub Actions API
& GitHub CPS Network Service
.

Gotchas
Disabled by enterprise administrators
In my enterprice the Hosted compute networking was disabled and I was unable to create new network configuration.
This was solved by heading into the policies for the enterprice and enabling creation of network configurations under the section Hosted compute networking
.


Spending limits
My spending limit was set to $0 and this caused the GitHub-hosted runner to never start.
This was solved by upping the spending limit in settings on the organization level.


Closing words
All files for this post can be found in this repository.
I original did this post in august 2024, but I never got around to publishing it so here it comes in july 2025 instead.