Azure IMDS attested data signatures can now chain through
Microsoft TLS G2 RSA CA OCSP intermediates, then through the
cross-signed Microsoft TLS RSA Root G2 certificate, before reaching
DigiCert Global Root G2.
coderd did not bundle the new G2 OCSP intermediates or the
cross-signed Microsoft TLS RSA Root G2 bridge certificate, so it could
fail to build a trusted chain for affected IMDS signatures.
Related to:
https://linear.app/codercom/issue/PLAT-205/bug-azure-instance-identity-verification-is-broken
Migrates Azure instance identity verification from
`go.mozilla.org/pkcs7` and `github.com/fullsailor/pkcs7` to
`github.com/smallstep/pkcs7`, using `VerifyWithChainAtTime` to validate
both the PKCS7 signature and the certificate chain in one call. The
previous code only verified the signer certificate against a set of
intermediates/roots but did not verify that the PKCS7 signature itself
covered the content, meaning tampered payloads could be accepted.
The `Options` struct is restructured to accept `Roots`, `Intermediates`,
and `CurrentTime` as explicit fields instead of embedding
`x509.VerifyOptions`. The test helper `NewAzureInstanceIdentity` now
builds a realistic 3-level certificate chain (Root CA -> Intermediate CA
-> Signing Cert) matching real Azure trust hierarchy. New tests
(`TestValidate_TamperedContent`,
`TestValidate_UntrustedCertWithValidSignature`) confirm tampered and
untrusted envelopes are rejected.
Addresses GHSA-6x44-w3xg-hqqf.
> [!NOTE]
> This PR was authored by Coder Agents.
<details>
<summary>Implementation Plan</summary>
### Files Changed
| File | Summary |
|------|---------|
| `coderd/azureidentity/azureidentity.go` | Replace `signer.Verify()`
with `VerifyWithChainAtTime`; restructure `Options` struct; add
`ParseCertificates()` helper |
| `coderd/azureidentity/azureidentity_test.go` | Add `testCertChain`
builder, tampered-content and untrusted-cert tests; update existing
tests for new `Options` API |
| `coderd/coderd.go` | Change `AzureCertificates` field from
`x509.VerifyOptions` to `azureidentity.Options` |
| `coderd/workspaceresourceauth.go` | Pass `api.AzureCertificates`
directly instead of wrapping |
| `coderd/coderdtest/coderdtest.go` | Migrate to `smallstep/pkcs7`;
build 3-level cert chain in test helper |
| `go.mod` / `go.sum` | Add `github.com/smallstep/pkcs7`; remove
`fullsailor/pkcs7` and `go.mozilla.org/pkcs7` |
</details>
Security improvements:
- Restrict cert fetches to a host+port allowlist (Microsoft and DigiCert
on 80/443).
- Route requests through a dedicated `http.Client` that resolves the
host once and dials the validated IP directly, preventing DNS rebinding.
- Reject loopback, private (RFC 1918 / IPv6 ULA), link-local, multicast,
unspecified, CGNAT, benchmarking, and IPv4-mapped IPv6 addresses.
- Cap the certificate response body at 1 MiB.
- Log the underlying error via slog and return a generic detail to the
caller to prevent information disclosure.
* fix(coderd): Harden Azure identity certificate fetch
- Restrict cert fetches to a host+port allowlist (Microsoft and
DigiCert on 80/443).
- Route requests through a dedicated `http.Client` that resolves
the host once and dials the validated IP directly.
- Reject loopback, private (RFC 1918 / IPv6 ULA), link-local,
multicast, unspecified, CGNAT, benchmarking, and IPv4-mapped
IPv6 addresses.
- Cap the certificate response body at 1 MiB.
- Log the underlying error via slog and return a generic detail
to the caller.
- Add unit tests for the URL allowlist, IP classification, and
dialer.
* fix(coderd/azureidentity): add IPv6 special-use ranges to SSRF blocklist
The extraBlockedNetworks list only contained IPv4 CIDRs. Add IPv6
equivalents that Go's stdlib classification methods do not cover:
- 64:ff9b:1::/48 RFC 8215 NAT64 translation
- 100::/64 RFC 6666 discard-only
- 2001:2::/48 RFC 5180 benchmarking
- 2001:db8::/32 RFC 3849 documentation
IPv6 ranges already handled by stdlib (unchanged):
- ::1/128 (IsLoopback)
- fc00::/7 (IsPrivate, ULA)
- fe80::/10 (IsLinkLocalUnicast)
- ff00::/8 (IsMulticast)
- ::/128 (IsUnspecified)
Fixes
[CODAGT-372](https://linear.app/codercom/issue/CODAGT-372/coderdazureidentity-testvalidateregular-fails-on-macos).
Closes coder/internal#101.
## Problem
`coderd/azureidentity TestValidate/regular` fails on macOS with:
```
verify signature:
github.com/coder/coder/v2/coderd/azureidentity.Validate
/Users/runner/work/coder/coder/coderd/azureidentity/azureidentity.go:75
- x509: “metadata.azure.com” certificate is not standards compliant
```
When `crypto/x509.VerifyOptions.Roots` is `nil`, Go's verifier on
macOS/iOS falls back to the system verifier (`systemVerify` in
`crypto/x509/root_darwin.go`), which delegates to Apple's
`SecTrustEvaluateWithError`. Apple's framework enforces stricter
standards-compliance checks than Go's pure-Go verifier and rejects some
otherwise valid Azure instance-identity leaf certificates with
`errSecCertificateIsNotStandardsCompliant`, surfaced as the `not
standards compliant` error.
The test had been skipped on darwin since #12979 (April 2024) as a
workaround.
## Fix
- Embed the three root CAs that Azure instance-identity certificates
ultimately chain to:
- DigiCert Global Root G2
- DigiCert Global Root G3
- Baltimore CyberTrust Root (kept for historical chains via `Microsoft
RSA TLS CA 01/02`)
- In `Validate`, populate `options.Roots` from those embedded roots when
the caller does not supply its own pool. Because `Roots != nil`, Go no
longer takes the `systemVerify` path on darwin and uses the pure-Go
verifier on all platforms.
- Remove the `runtime.GOOS == "darwin"` skip from `TestValidate`.
- Add `TestEmbeddedRoots` to guard against future regressions in the
embedded root list (parses each PEM, asserts self-signed, requires all
three named roots).
The caller's existing `Intermediates` handling is unchanged. Tests that
pass their own `Roots` (e.g. `coderdtest.NewAzureInstanceIdentity`) are
unaffected.
## Verification
On Linux:
```
$ go test ./coderd/azureidentity/ -race -count=1 -v
=== RUN TestValidate
=== RUN TestValidate/regular
=== RUN TestValidate/govcloud
=== RUN TestValidate/rsa
--- PASS: TestValidate (0.00s)
--- PASS: TestValidate/regular (0.00s)
--- PASS: TestValidate/rsa (0.00s)
--- PASS: TestValidate/govcloud (0.00s)
=== RUN TestEmbeddedRoots
--- PASS: TestEmbeddedRoots (0.00s)
=== RUN TestExpiresSoon
--- SKIP: TestExpiresSoon (0.00s)
PASS
ok github.com/coder/coder/v2/coderd/azureidentity 1.020s
```
The `test-go-pg` job on `macos-latest` in CI is the authoritative
confirmation of the fix on macOS; previously it would have failed
`TestValidate/regular` had the skip been removed.
<details>
<summary>Why this is the correct fix</summary>
From `/usr/local/go/src/crypto/x509/verify.go`:
```go
// Use platform verifiers, where available, if Roots is from SystemCertPool.
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
systemPool := systemRootsPool()
if opts.Roots == nil && (systemPool == nil || systemPool.systemPool) {
return c.systemVerify(&opts)
}
...
}
```
Setting `opts.Roots` to any non-nil, non-system pool deterministically
routes verification through Go's pure-Go verifier, bypassing Apple's
stricter compliance checks. The embedded roots are sufficient to
validate every chain we currently care about, since every intermediate
in `Certificates` ultimately issues to one of the three embedded roots.
</details>
> Generated by Coder Agents. Reviewed manually.
* chore: add /v2 to import module path
go mod requires semantic versioning with versions greater than 1.x
This was a mechanical update by running:
```
go install github.com/marwan-at-work/mod/cmd/mod@latest
mod upgrade
```
Migrate generated files to import /v2
* Fix gen