Rebasing onto main pulled in TestK8sOperator and the OAuth client CLI tests; regenerate integration/tests.nix so the NixOS-VM check matrix runs them too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxukxKem3HU4i5CqNrvvdj
Garnix builds every flake check and package directly, so the GitHub
Actions wrappers around them are redundant:
- nix-checks checks.{build,gotest,golangci-lint,formatting}
- nix-module-test checks.headscale (NixOS module test)
- test-integration the integration-* NixOS-VM check matrix
- build packages.headscale (nix build)
Kept are the pipelines garnix cannot reproduce: servertest (excluded
from the flake), integration-legacy (needs real DERP, so it cannot run
hermetically), and the check-generated / check-tests generators that
keep the nix integration matrix and protobuf output in sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxukxKem3HU4i5CqNrvvdj
CI runs nix build .#checks.<system>.integration-* instead of the bespoke docker
matrix — the generated checks are the matrix. The tests the hermetic VM can't
run (real-internet DERP, docker cable-pull) run via cmd/hi in a slim legacy
workflow, which also keeps cmd/hi and the integration Dockerfiles exercised.
check-tests guards the pinned versions against capver drift.
Each test becomes a hermetic check: a VM boots docker, loads the prebuilt images
and runs that one test (nix/tests/integration.nix), so the suite runs offline as
nix build .#checks.<name>. The generator emits the test list as nix: heavy
multi-subtest tests are split into per-subtest checks, and tests the nested
docker VM can't host are excluded for the legacy pipeline. flake.nix wires the
checks, the images, and the Go build/test/lint checks (with the version JSON on
the embed whitelist).
Read the HEADSCALE_INTEGRATION_*_IMAGE knobs through one envknob helper and
assert required tooling is present instead of installing it at runtime (the
nix VM is offline). dsic waits for both the derper cert and key before starting
derper, so the container can't die between the two uploads. The nested-docker
VM is slower than a host daemon, so every integration timeout (convergence,
docker exec, peer sync, pool wait) now flows through one
dockertestutil.ScaleTimeout honouring HEADSCALE_INTEGRATION_TIMEOUT_SCALE.
Replaces the Dockerfile builds for the flake-check path. headscale is built
from the Go-filtered source; one tailscale-HEAD image serves client, daemon,
derper and the websocket-DERP test. Released tailscale versions and postgres
are pinned registry pulls, arch following the build system. Tools the suite
used to apk-add (curl/ssh/dig/python3) and the SSH login users are baked in at
build time so no container is mutated at startup, and busybox su is included so
taildrive can fork its per-user fileserver.
The hermetic nix checks cannot pull tailscale images at eval time, so the tested
version set and the per-arch image digests are pinned in
integration/tailscale-versions.json. capver.IntegrationVersions() is the single
source the suite, the drift guard and the pin generator share; the generator
(tools/tailscale-versions, run via go generate) refreshes it after a capver bump
and reuses already-present pins so a routine generate makes no network call.
It needs no special runner (every integration container already runs privileged), so it joins the generated matrix; load br_netfilter only for that test.
Updates #1202
Run the real operator in a single-container k3s cluster against an in-test Headscale over plain HTTP. k3sic exposes reusable building blocks (InstallOperator, DeployConnector, DeployEchoServer, ExposeServiceToTailnet, DeployProxyGroup); the test covers operator registration, an egress connector, ingress connectivity from a tailnet node, and proxy groups. tls-ca-baking.md records the private-CA TLS variant.
Updates #1202
Mints an admin API key and creates a keyType=client OAuth credential through gen/client/v2, exposed on ControlServer. Reusable by tests needing OAuth client credentials.
Sets TS_DEBUG_DERP_WS_CLIENT + TS_DEBUG_USE_DERP_HTTP so a client can reach hsic's plain-HTTP embedded DERP over websockets; the counterpart to hsic.WithoutTLS.
Defence-in-depth fixes to the OIDC login flow.
Set the state/nonce cookie Secure flag from the configured server_url
scheme rather than req.TLS, so the cookies stay Secure behind a
TLS-terminating reverse proxy where the proxy-to-Headscale hop is plain
HTTP. Deriving it from config avoids trusting a spoofable
X-Forwarded-Proto header.
Make the OIDC state single-use: consume it from the cache on the
callback and clear the state/nonce cookies once validated, so a replayed
callback cannot resolve the same session and the cookies do not linger
until expiry.
Bound OIDC discovery to the caller's context so a slow or unreachable
issuer fails startup within the timeout instead of hanging, and validate
the issuer URL and required client_id/client_secret at config load so an
unworkable setup fails fast.
Add the OAuth client type, its database storage, the scope grant package,
policy tag-ownership exposure, and the state operations backing the v2
OAuth client-credentials flow.
NewAuthProviderOIDC received a context with a 30-second timeout from
its caller, but immediately discarded it by passing context.Background()
to oidc.NewProvider. This caused both `headscale serve` and
`headscale configtest` to hang indefinitely if the OIDC issuer was
unreachable, rather than timing out and returning an error.
Fix by passing the caller's ctx to oidc.NewProvider and remove the
nolint:contextcheck annotation that was suppressing the linter warning
about this bug. The annotation was introduced in ce580f82 as part of a
bulk lint-suppression pass without addressing the underlying issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercises the user data sources via the Go SDK, tscli, and OpenTofu, and
backfills tailscale_4via6 (provider-local compute), each with the
no-drift gate.
Pins the wire shape: honest constants, deviceCount aggregation, the
{"users":[...]} envelope, ignored type/role filters, and 404 on
unknown/non-numeric/pseudo-user ids.
Backs the tailscale_user and tailscale_users data sources. getUser maps
gorm/ErrUserNotFound to 404; unmodelled fields are honest constants,
deviceCount/lastSeen/currentlyConnected aggregate the user's nodes.
Tailscale's keys API has no separate expire verb: DELETE is the revoke.
Map it to a soft revoke so the key stays retrievable as invalid afterwards
instead of vanishing, matching the SDK and Terraform's expectations.
Add a revoked timestamp to pre-auth keys (migration plus schema), mark a
key invalid once revoked, and have the keys DELETE handler stamp it rather
than destroy the row. A background collector reaps revoked keys after a
configurable retention window (preauth_keys.revoked_retention, default
168h), so the table does not grow without bound.
Add the v2 foundation — Basic/Bearer auth, scope scaffolding, the
tailnet check, and the Tailscale error shape — plus the auth-key
endpoints mapped onto Headscale pre-auth keys.
Give API keys an optional owning user and pre-auth keys a free-text
description, plus the state accessors the v2 API needs to act as a
key's owner and to persist descriptions.
Express the maximum SSH check period as 7 * 24 * time.Hour instead of a
bare nanosecond constant, matching how the rest of the code spells out
magic durations.
Serialize node, user and pre-auth-key responses through the NodeView,
UserView and PreAuthKeyView accessors instead of AsStruct(), which clones
the whole record on every read. Document the convention in AGENTS.md.
Add StringID() on Node/NodeView and Username/Display/CreatedAt on
UserView, so the HTTP read paths render ids and read users through view
accessors instead of cloning with AsStruct().