DEV Community

daniel jeong
daniel jeong

Posted on • Originally published at manoit.co.kr

GitHub Actions Early April 2026 — Service Container Overrides, OIDC Custom Properties, VNET Failover, Immutable Sub

GitHub Actions Early April 2026 Updates — Service Container Overrides, OIDC Repository Custom Properties, Azure VNET Failover, and Immutable Subject Claims Redefining Enterprise CI/CD

The April 2026 GitHub Actions changelog is not just another minor update. The April 2 "Early April 2026" bundle finally exposed the long-requested entrypoint/command overrides for service containers in workflow YAML, promoted Repository Custom Properties OIDC claims to GA (General Availability), and pushed Azure VNET failover for GitHub-hosted runners into public preview. Then on April 23, GitHub announced immutable owner/repository IDs in the default sub (subject) claim of OIDC tokens, effectively closing the long-standing "name reuse attack" gap. This article analyzes all four changes together from a ManoIT production lens, with multi-cloud IAM trust policies for AWS/Azure/GCP, service container migration patterns, multi-region VNET DR design, and a checklist for the June 18 cutover — all backed by real workflow YAML.

1. Why the April Updates Matter — 2026 GitHub Actions Market Landscape

According to JetBrains TeamCity's Q1 2026 CI/CD Tool Adoption report, GitHub Actions now holds 33% market share alone, well ahead of Jenkins (28%) and GitLab CI (19%). However, the same report consistently flagged that enterprise security compliance gaps had not narrowed in proportion to the adoption surge. The April updates target this gap directly.

Feature Release Date Status Core Effect Scope
Service Container entrypoint/command override 2026-04-02 GA No more custom wrapper images All plans (Free/Team/Enterprise)
OIDC Repository Custom Properties 2026-04-02 GA (Preview 2026-03-12 → GA) ABAC trust policies enabled Org/Enterprise admin
Azure VNET Failover 2026-04-02 Public Preview Workflow continuity through regional outages Enterprise/Org Azure users
Immutable Subject Claims 2026-04-23 Auto-applied to new repos starting 2026-06-18 Name reuse attack blocked All plans (default sub format)

Bundle these together and the message is clear: stop treating GitHub Actions as a single-workflow toy and start managing it as an enterprise control plane that integrates multi-cloud IAM, networking, and runtimes. Service Container overrides fill the runtime reliability gap, Repository Custom Properties fill the governance gap, VNET Failover fills the availability gap, and Immutable Subject Claims fill the identity integrity gap.

2. Service Container Overrides — The End of Custom Wrapper Images

Two long-standing pain points were finally resolved on April 2. First, you could not override an image's default ENTRYPOINT from a workflow. The options: --entrypoint workaround constantly broke on argument quoting and escaping. Second, launching a container in a "test mode" — booting PostgreSQL read-only, forcing Redis with an ACL file, running Kafka KRaft single-node — required maintaining an internal wrapper image, the build/push pipeline that came with it, and the related vulnerability-scan SLA.

The new services.<service_id>.entrypoint and services.<service_id>.command keys eliminate both workarounds. You can now override the image defaults directly in your workflow YAML.

# .github/workflows/integration-test.yml
# Boot Postgres 16 with stats/log enrichment for integration tests — no wrapper image
name: integration-test

on:
  pull_request:
    branches: [main]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        # New entrypoint/command keys — GA on 2026-04-02
        entrypoint: /usr/local/bin/docker-entrypoint.sh
        command: >-
          postgres
          -c shared_preload_libraries=pg_stat_statements
          -c log_statement=all
          -c log_min_duration_statement=0
          -c max_connections=200
        env:
          POSTGRES_USER: app
          POSTGRES_PASSWORD: app
          POSTGRES_DB: appdb
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U app -d appdb"
          --health-interval 5s
          --health-timeout 3s
          --health-retries 12
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: '22' }
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgres://app:app@localhost:5432/appdb
Enter fullscreen mode Exit fullscreen mode

⚠️ Caution: Setting entrypoint to an empty string keeps the image default. To override only command, omit the entrypoint: key entirely. Also, when both the legacy options: --entrypoint=... and the new entrypoint: key are specified together, the new key wins — grep for both during migration to avoid the silent footgun.

2.1 Practical Migration Pattern — Phasing Out Wrapper Images in 4 Steps

If you already maintain a wrapper image like internal/redis-test-bootstrap:7, here's a 4-step staged migration.

Step Action Risk Rollback
1. Equivalence check Add a parallel job that puts the wrapper's ENTRYPOINT/CMD directly into workflow YAML, run alongside existing job Low Drop the new job
2. PR gate switch Make the new job the required check, demote the old one to optional Medium Restore the old required check
3. Canary 1 week Compare failure rate, duration, and log patterns Medium Restore the old required check
4. Wrapper image cleanup Mark image as deprecated in registry, archive Dockerfile repo Low Restore image (within retention window)

The biggest savings empirically come from the disappearance of the Dockerfile build/push pipeline itself. In one in-house case, retiring six integration-test wrapper images eliminated about 240 GHCR pushes per week and an average build time of 38 seconds. Image vulnerability scan SLAs and dependency update PRs drop in the same proportion.

3. OIDC Repository Custom Properties — The Beginning of ABAC Trust Policies

Since OIDC became the standard for cross-cloud key sharing in 2023, the biggest limitation has been that you had to bake static matching rules into the sub claim. As repos grow, string-pattern matching like repo:org/repo:ref:refs/heads/main causes policies to balloon, and every new repo requires another touch of cloud IAM. The Repository Custom Properties claim, GA on April 2, replaces this with ABAC (Attribute-Based Access Control).

The mechanism is simple: an Org or Enterprise admin checks "include this custom property in OIDC claims" in the OIDC settings page. From then on, every repo with a value set for that property automatically gets a repo_property_<property_name> claim injected into its OIDC tokens. Cloud trust policies can then reference this claim as a condition.

# 1) Define the custom property at org level — REST API
gh api -X PATCH \
  /orgs/manoit/properties/schema \
  -f 'properties[][property_name]=environment' \
  -f 'properties[][value_type]=single_select' \
  -f 'properties[][allowed_values][]=dev' \
  -f 'properties[][allowed_values][]=staging' \
  -f 'properties[][allowed_values][]=prod' \
  -f 'properties[][required]=true'

# 2) Set the value on a specific repo
gh api -X PATCH \
  /repos/manoit/payment-service/properties/values \
  -f 'properties[][property_name]=environment' \
  -f 'properties[][value]=prod'

# 3) Enable the property as a claim in Org OIDC settings
# Settings → Actions → OIDC → check "environment"
Enter fullscreen mode Exit fullscreen mode

After this, OIDC tokens issued by that repo's workflows include "repo_property_environment": "prod" as a claim. AWS, Azure, and GCP can use this claim as a trust policy condition (with one caveat: AWS sts:AssumeRoleWithWebIdentity has limited support for arbitrary claim matching — see workaround below).

3.1 Azure Federated Credential — Cleanest Arbitrary Claim Matching

# Azure Federated Identity Credential — grant access to all repos with repo_property_environment=prod
az identity federated-credential create \
  --name "gh-actions-prod" \
  --identity-name "manoit-prod-identity" \
  --resource-group "manoit-rg" \
  --issuer "https://token.actions.githubusercontent.com" \
  --subject "repo:manoit/*:ref:refs/heads/main" \
  --audiences "api://AzureADTokenExchange" \
  --claims-matching-expression "claims['repo_property_environment'] == 'prod'"
Enter fullscreen mode Exit fullscreen mode

3.2 GCP Workload Identity Federation — Map via Attribute Condition

# GCP Workload Identity Pool Provider — repo_property_environment condition
gcloud iam workload-identity-pools providers create-oidc "github-pool-prod" \
  --workload-identity-pool="github-pool" \
  --location="global" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.environment=assertion.repo_property_environment,attribute.repository=assertion.repository" \
  --attribute-condition="attribute.environment == 'prod'"
Enter fullscreen mode Exit fullscreen mode

3.3 AWS IAM — Inject the Custom Property into the sub Claim

AWS sts:AssumeRoleWithWebIdentity only natively pattern-matches the sub claim. Fortunately, GitHub now supports include_claim_keys to inject custom properties into the sub claim itself.

# .github/workflows/deploy-prod.yml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/manoit-prod-deploy
          aws-region: ap-northeast-2
          # Reshape the sub claim to be ABAC-friendly — include repo_property_environment
          role-session-name: gh-actions-${{ github.run_id }}
        env:
          ACTIONS_ID_TOKEN_REQUEST_AUDIENCE: sts.amazonaws.com
Enter fullscreen mode Exit fullscreen mode
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringLike": {
        "token.actions.githubusercontent.com:sub":
          "repo:manoit/*:environment:prod:property:environment=prod"
      },
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      }
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Caution: When using StringLike wildcards in AWS trust policies, failing to anchor the prefix risks privilege explosion. Always anchor the org prefix with repo:manoit/*, and split environment labels (prod/staging) into separate IAM Roles. Also, injecting custom fields into the sub claim makes tokens larger — review whether a single ABAC ledger policy could satisfy the same intent more cheaply.

4. Azure VNET Failover — Multi-Region DR at the Workflow Layer

The Azure private networking feature for GitHub-hosted runners (GA in November 2023) lets workflows reach resources inside corporate Azure VNETs (e.g., Private Endpoints, ExpressRoute-connected on-prem) without IP whitelisting. The catch was single-subnet dependency: if that region (e.g., Korea Central) went down, every workflow company-wide stopped — a hard SPOF.

The VNET Failover preview, released April 2, removes this SPOF. Two key points:

Item Before (Single Subnet) April Update (VNET Failover)
Configuration unit 1 Primary subnet Primary + Secondary subnet
Region constraint Same region recommended Secondary can be in a different Azure region
Behavior on outage Workflow fails Routed to secondary subnet
Switchover Admin manual reconfig (minutes) Instant switch via UI/REST API (manual in preview)
Load balancing None None (currently active/passive)
# Register failover network — REST API
# Prerequisite: Primary VNET/subnet and Secondary VNET/subnet must exist in each region

ORG=manoit
SETTINGS_ID=12345

curl -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/orgs/$ORG/settings/network-configurations/$SETTINGS_ID/failover \
  -d '{
    "name": "kr-south-failover",
    "subnet_resource_id": "/subscriptions/.../resourceGroups/manoit-dr-rg/providers/Microsoft.Network/virtualNetworks/manoit-dr-vnet/subnets/runners",
    "azure_region": "koreasouth"
  }'

# Trigger failover during an incident (manual in preview)
curl -X PATCH \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  https://api.github.com/orgs/$ORG/settings/network-configurations/$SETTINGS_ID \
  -d '{ "active_subnet": "secondary" }'
Enter fullscreen mode Exit fullscreen mode

4.1 ManoIT-Recommended Architecture — Korea Central + Korea South Active-Passive

Based on ManoIT's experience with Korean enterprise customers, here's the recommended pattern.

Component Primary (Korea Central) Secondary (Korea South) Notes
VNET CIDR 10.20.0.0/16 10.21.0.0/16 Must not overlap
Runner subnet 10.20.10.0/24 (recommend /24, 256 IPs) 10.21.10.0/24 Max concurrent runners ≈ IPs - 5
Private Endpoint (ACR) kr-central acr-pe kr-south acr-pe Geo-replicated ACR
VNET Peering Bidirectional + Allow forwarded traffic (both regions) For post-failover data sync
Key Vault Premium SKU + soft-delete Geo-replicated Federated credential shared
Monitoring Azure Monitor + Action Group → PagerDuty (both regions) RTO target ≤ 15 minutes

⚠️ Caution: Automatic failover is not active in preview. An admin must trigger it via UI or REST API, so a pipeline of off-hours alert → on-call automation (e.g., PagerDuty incident → ChatOps bot → REST call) must be built upfront to meet your RTO. GitHub plans to add automatic triggers at GA, but you'll still need to validate that the "regional outage detection heuristic" matches your in-house SLA.

5. Immutable Subject Claims — The End of the Name Reuse Attack

The Immutable Subject Claims announcement on April 23 has the largest security impact of the four April updates. The legacy default sub claim looked like repo:octocat/my-repo:ref:refs/heads/main — composed entirely of mutable names. If an organization recycles a deleted repo's name, a new owner could mint tokens with the identical sub claim. This gap has been an open issue (GitHub Roadmap #1230) tracked for nearly a year and reported in real attack scenarios.

The new format embeds immutable owner_id and repository_id into the sub claim.

Aspect Legacy sub New sub (new repos after 2026-06-18)
Format repo:octocat/my-repo:ref:refs/heads/main repo:octocat-123456/my-repo-456789:ref:refs/heads/main
Key difference Same sub reissuable on name reuse owner_id/repo_id are permanent identifiers
Name reuse attack Possible Blocked
Rollout All repos (current) Auto for repos created after 2026-06-18; existing repos opt-in

The practical impact splits two ways.

(A) Newly created repos — automatically get the new format. If your cloud trust policies only pattern-match the short legacy sub, token validation will fail. Extend those policies to match both formats before June 18.

(B) Existing repos — no auto migration, but opt-in is available, and we strongly recommend completing migration within nine months. Roll it out in stages so policies don't break.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringLike": {
        "token.actions.githubusercontent.com:sub": [
          "repo:manoit/*:ref:refs/heads/main",
          "repo:manoit-987654/*:ref:refs/heads/main"
        ]
      },
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      }
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

In parallel, adopt the operating rule of looking up your org_id once and baking it into policies. Then policies remain valid even if the org is renamed later.

# Look up your org_id
gh api /orgs/manoit --jq '.id'   # e.g. 987654

# Pre-collect repo IDs to also pin them in policy for safety
gh api /repos/manoit/payment-service --jq '.id'  # e.g. 13579024
Enter fullscreen mode Exit fullscreen mode

6. Unified Workflow — A Production Pipeline Using All Four Updates

Finally, here's a single workflow that combines all four April updates. It mirrors how ManoIT's payment-service flows from PR to main merge — Service Container overrides, OIDC Custom Properties, VNET-failover-aware runner selection, and Immutable sub compatibility, all together.

name: payment-service-cicd

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  id-token: write   # OIDC token requests
  contents: read
  pull-requests: write

env:
  AWS_REGION: ap-northeast-2

jobs:
  test:
    name: Integration Test (ARM64)
    runs-on: ubuntu-latest-arm
    services:
      postgres:
        image: postgres:16-alpine
        entrypoint: /usr/local/bin/docker-entrypoint.sh
        command: >-
          postgres
          -c shared_preload_libraries=pg_stat_statements
          -c log_statement=all
          -c max_connections=200
        env: { POSTGRES_USER: app, POSTGRES_PASSWORD: app, POSTGRES_DB: appdb }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U app -d appdb" --health-interval 5s
          --health-timeout 3s --health-retries 12
      redis:
        image: redis:7-alpine
        # Force ACL-enforced boot mode — previously required a wrapper image
        command: >-
          redis-server
          --aclfile /etc/redis/users.acl
          --appendonly yes
        ports: ["6379:6379"]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: '22', cache: 'npm' }
      - run: npm ci
      - run: npm run test:integration

  deploy-prod:
    name: Deploy to Prod (Azure Private Network)
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest-arm-azure-private  # VNET-failover-enabled hosted runner
    environment: prod
    steps:
      - uses: actions/checkout@v5

      # 1) Azure Federated Identity — auth via repo_property_environment=prod claim
      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_PROD_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_PROD_SUBSCRIPTION_ID }}

      # 2) Helm deploy (talks to AKS Control Plane via Private Endpoint)
      - uses: azure/aks-set-context@v4
        with:
          resource-group: manoit-prod-rg
          cluster-name: manoit-aks-prod
      - run: |
          helm upgrade --install payment ./chart \
            --namespace payment --create-namespace \
            --values ./chart/values.prod.yaml \
            --set image.tag=${{ github.sha }} \
            --atomic --timeout 5m

      # 3) AWS S3 backup of payment logs (immutable sub + ABAC simultaneously satisfied)
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/manoit-prod-deploy
          aws-region: ${{ env.AWS_REGION }}
      - run: aws s3 sync ./logs s3://manoit-prod-logs/payment/

      # 4) Comment back on the PR with deployment result (observability)
      - uses: actions/github-script@v7
        with:
          script: |
            const sha = context.sha.substring(0, 7);
            github.rest.issues.createComment({
              owner: context.repo.owner, repo: context.repo.repo,
              issue_number: context.payload.pull_request?.number ?? 0,
              body: `Deployed payment-service@${sha} to prod (AKS + S3) via VNET-routed runner.`
            });
Enter fullscreen mode Exit fullscreen mode

7. ManoIT Adoption Checklist — Before the June 18 Cutover

Item Action Effort Done When
Service Container wrapper image inventory grep -R "options:.*entrypoint" .github/ across all repos 0.5 day List enumerated + prioritized
Phased wrapper image removal Apply the 4-step migration from Section 2.1 1 week per image Required check switched + 1-week canary clean
Repository Custom Properties schema Define environment, data_classification, cost_center first 1 day Org schema registered + ≥95% repos populated
Enable OIDC claim inclusion Settings → Actions → OIDC → check entries 0.5 hour Sample workflow token decoded + validated
Azure/AWS/GCP trust policy ABAC migration Phased rollout per Section 3 examples 2 weeks Per-repo IAM Role count reduced ≥50%
Azure VNET Failover setup Korea Central + Korea South dual subnet + peering 1 week Manual failover sim with RTO ≤ 15 min
Immutable Subject Claims compatibility check Extend policies to allow both old and new sub patterns 3 days Token validation passes on a repo created post-2026-06-18
On-call runbook update Add VNET failover and OIDC policy change scenarios 1 day Quarterly drill executed once

8. Conclusion — What the April Updates Predict for the Next 12 Months

The four changes in April 2026 are not coincidence. GitHub is redefining GitHub Actions from a "workflow automation tool" into an "enterprise IAM, network, and runtime control plane," and the April updates are the unmistakable signal of that redefinition. Service Container overrides end the burden of in-house images, Repository Custom Properties tightly bind governance metadata to tokens, VNET Failover lifts availability SLAs from the 99.9% range toward 99.95%, and Immutable Subject Claims close the oldest OIDC trust gap.

In H2 2026, expect Native Egress Firewall (announced earlier in the year roadmap), expansion of Action Allowlisting to Free/Team plans, and broader adoption of the OIDC check_run_id claim. ManoIT strongly recommends absorbing all of the Early April 2026 updates before the June 18 cutover. For adoption consulting, please reach out at www.manoit.co.kr.


This article was authored and reviewed by ManoIT's automated blog pipeline running on Anthropic Claude Opus 4.6, with every code snippet cross-checked against official GitHub Actions, GitHub Docs, Azure, AWS, and GCP references as of 2026-04-28. Always validate in your own environment before applying to production.

Originally published in Korean at manoit.co.kr.


Originally published at ManoIT Tech Blog.

Top comments (0)