I Converted a Kubernetes Cluster to Dual-Stack So git Could Go IPv6-Only — Then AWS Said No
A self-hosted GitLab can't go inbound-IPv6-only as long as it's your AWS IAM OIDC provider. The reason is easy to miss until you know it: AWS STS fetches the verification key (JWKS) for every OIDC login exclusively over IPv4 — no A record, no AssumeRoleWithWebIdentity, every CI pipeline that logs into AWS via OIDC dead. I'd set out to remove public IPv4 from the whole estate step by step, and the last piece was the GitLab box serving git. and registry.. The change was supposed to be a one-liner: drop the A records, close v4 in the security group, done. It wasn't — twice over. This post walks through both blockers, how I tracked them down, and why the second one teaches more about OIDC trust models than any documentation does.
TL;DR
TL;DR: A self-hosted GitLab can't go inbound-IPv6-only as long as it's your AWS IAM OIDC provider. AWS STS fetches the OIDC verification key (JWKS) exclusively over IPv4. No A record → no AssumeRoleWithWebIdentity → every CI pipeline that talks to AWS via OIDC breaks.
The plan: less public IPv4
Public IPv4 addresses now cost money on AWS — and every open v4 address is attack surface. So I removed IPv4 from the estate step by step: the CI worker fleet runs IPv6-only (egress via NAT66), registries and package sources run dual-stack, and a small dedicated pool keeps v4 only for the jobs that demonstrably need it.
The last piece: put the GitLab box, which serves both git. and registry., on inbound IPv6-only. The box itself has long been IPv6-capable — global address, default v6 route, working v6 egress, curl -6 returns 200. Should have been a one-liner: drop the A records, close v4 in the security group, done — it wasn't, twice over.
Blocker 1: the pod network was single-stack
First attempt, rolled back immediately. Production k3s ran single-stack IPv4 — without --cluster-cidr/--service-cidr flags, so with the defaults, and pods only got an IPv4 address. ArgoCD's repo-server pod simply couldn't reach an AAAA-only git (network is unreachable) — every GitOps sync stalled.
The crux: the service CIDR is immutable after cluster init. Retrofitting dual-stack means rebuilding the cluster. I did that as a blue/green rebuild — new node, dual-stack from the start, data moved over, DNS switched, old node left standing as a rollback. A nice side effect: a real disaster-recovery drill under controlled conditions, downtime around 15 minutes.
Now the pods had IPv6, ArgoCD synced over v6 — blocker 1 done. So git went back to IPv6-only, this time it had to work.
The twist: GitLab is the OIDC provider
Minutes after the switch, every tofu apply pipeline turned red:
Error: failed to retrieve credentials … operation error STS:
AssumeRoleWithWebIdentity … InvalidIdentityToken: Couldn't retrieve
verification key from your identity provider
And not just Terraform — the GitLab patch automation and container signing too. Everything that talked to AWS via OIDC was dead.
The reason, once you see it, is obvious: CI authenticates to AWS keylessly via GitLab as the OIDC provider. The flow:
- The CI job gets a signed OIDC token from GitLab.
- It calls
sts:AssumeRoleWithWebIdentitywith that token. - AWS STS validates the signature — and to do so, fetches the provider's public key from its JWKS endpoint:
git.…/oauth/discovery/keys.
Step 3 is the catch. That fetch comes from AWS' infrastructure outbound — over IPv4. Once git. has no A record left, AWS can't load the key. The token is technically fine; AWS just can't verify it. Result: InvalidIdentityToken.
The nasty part: AWS caches the key for a while. The apply run right after the switch still went green — the next one, after the cache expired, didn't. Nine minutes apart. If you only see the first one, you'd think the migration worked.
How to pin it down
Two commands are enough:
# Does the name still resolve on v4?
dig +short A git.example.eu # → empty
# Is the JWKS endpoint reachable over v4? (what STS does)
curl -4 git.example.eu/oauth/discovery/keys # → 000 (fail)
curl -6 git.example.eu/oauth/discovery/keys # → 200 (ok)
v6 answers, v4 doesn't, AWS can only do v4 — diagnosis done. The fix was the revert (A records and v4 ingress back), and since CI can't fix itself — the fix is an apply — I had to run it once outside the pipeline.
The lesson
It's the same lesson as blocker 1, just with a consumer that's easy to miss:
Before you drop an A record, verify for every consumer that it can reach the name over IPv6 — including external SaaS like AWS STS, not just the obvious clients on your own network.
“The host is IPv6-capable” isn't enough. What matters is whether everyone who talks to it can reach it over IPv6. An OIDC provider is, by definition, something validated from the outside — and that validator isn't yours.
The way out
git can go inbound-IPv6-only — but only once no external AWS caller needs to reach it over IPv4 anymore. Concretely: stop having CI jobs assume the apply role via GitLab OIDC, and instead use the CI runner's EC2 instance role — a caller inside AWS, with no external JWKS fetch at all. Until then, git stays dual-stack. Roughly €3.60 a month for an Elastic IP isn't a reason to break half of CI.
The dual-stack cluster was worth the effort anyway — just for a different reason than planned.
Frequently asked questions
What's the long-term fix so git can go IPv6-only after all?+
Stop having CI jobs assume the apply role via GitLab OIDC, and use the CI runner's EC2 instance role instead — a caller inside AWS that needs no external JWKS fetch. Once no external AWS caller needs to reach git over IPv4 anymore, the switch to inbound-IPv6-only can be repeated without this blocker.
How do I spot this problem before I drop an A record myself?+
Two commands: dig +short A <host> shows whether an IPv4 entry still exists, and comparing curl -4 against curl -6 on the relevant endpoint (e.g. the JWKS discovery path) shows whether the service is actually reachable over IPv6. The key is checking this for every consumer that needs the address — not just the obvious clients on your own network.
Why couldn't dual-stack just be retrofitted onto the running cluster?+
In k3s, the service CIDR is immutable after cluster init. Once the cluster was initialized single-stack IPv4 — here without setting the --cluster-cidr/--service-cidr flags, so with the IPv4 defaults — the only way to get dual-stack cleanly is a rebuild.
Why did the first tofu apply after the switch still go green?+
AWS STS caches the JWKS key for a while. The apply right after the switch still hit the cached key and went green — only the next apply, roughly nine minutes later once the cache expired, failed. If you only check the first test run, you'll wrongly conclude the migration succeeded.
Does this only affect GitLab as an OIDC provider, or other identity providers too?+
Structurally, it affects any self-hosted OIDC provider whose JWKS endpoint an external cloud verifier like AWS STS has to reach over the network — that's a property of the OIDC trust model, not a GitLab quirk. Whether other identity providers or other cloud vendors use the same IPv4-only fetch path is something we haven't verified individually here; anyone planning a similar migration should test it for their own provider/cloud combination.
Why not just stay dual-stack instead of forcing IPv6-only?+
Public IPv4 now costs money on AWS, and every open v4 address expands the attack surface — hence the plan to get rid of it step by step. But for git there came a point where the benefit (one saved Elastic IP at roughly €3.60/month) didn't justify the cost (OIDC pipelines failing outright). Until the OIDC trust path is rebuilt, git deliberately stays dual-stack.
Conclusion
Two blockers, one lesson: “IPv6-capable” is a property of the host, not of the migration. What matters is whether every consumer — the obvious one on your own network as much as the easy-to-miss external SaaS validator — can reach the name over IPv6 before the A record goes. The blue/green cluster rebuild was the right call and delivered a real DR drill for free. git stays dual-stack for now, until CI assumes its AWS role via the instance role instead of GitLab OIDC — then the external JWKS fetch goes away, and IPv6-only becomes possible again.
I help with your IPv6 migration and Kubernetes dual-stack rebuilds — including the consumers that are easy to overlook.
DNS and network audit before the A-record drop, cluster-CIDR planning for rebuilds, and checking external dependencies like OIDC providers, webhooks, or SaaS integrations for IPv6 reachability.
Platform operations, not consulting on paper: I audit, rebuild, and harden your infrastructure on an ongoing basis.
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
