From 00da5361b3e3af966925048f9194b90eadf76957 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 7 Jan 2026 12:13:04 +0100 Subject: [PATCH] integration: test tags-only auth key behavior Add tests for auth keys without user ownership to verify tags from key are used regardless of --advertise-tags flag. --- .github/workflows/test-integration.yaml | 2 + integration/tags_test.go | 146 ++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 0ce68322..433145de 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -240,6 +240,8 @@ jobs: - TestTagsIssue2978ReproTagReplacement - TestTagsAdminAPICannotSetInvalidFormat - TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags + - TestTagsAuthKeyWithoutUserInheritsTags + - TestTagsAuthKeyWithoutUserIgnoresAdvertisedTags uses: ./.github/workflows/integration-test-template.yml secrets: inherit with: diff --git a/integration/tags_test.go b/integration/tags_test.go index 687236ce..952aebaa 100644 --- a/integration/tags_test.go +++ b/integration/tags_test.go @@ -2979,3 +2979,149 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) { }, 60*time.Second, 1*time.Second, "verifying tags removed and ownership returned") }) } + +// ============================================================================= +// Test Suite 5: Auth Key WITHOUT User (Tags-Only Ownership) +// ============================================================================= + +// TestTagsAuthKeyWithoutUserInheritsTags tests that when an auth key without a user +// (tags-only) is used without --advertise-tags, the node inherits the key's tags. +// +// Test 5.1: Auth key without user, no --advertise-tags flag +// Setup: Run `tailscale up --auth-key AUTH_KEY_WITH_TAGS_NO_USER` +// Expected: Node registers with the tags from the auth key. +func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) { + IntegrationSkip(t) + + policy := tagsTestPolicy() + + spec := ScenarioSpec{ + NodesPerUser: 0, + Users: []string{tagTestUser}, + } + + scenario, err := NewScenario(spec) + + require.NoError(t, err) + defer scenario.ShutdownAssertNoPanics(t) + + err = scenario.CreateHeadscaleEnv( + []tsic.Option{}, + hsic.WithACLPolicy(policy), + hsic.WithTestName("tags-authkey-no-user-inherit"), + hsic.WithTLS(), + ) + requireNoErrHeadscaleEnv(t, err) + + headscale, err := scenario.Headscale() + requireNoErrGetHeadscale(t, err) + + // Create an auth key with tags but WITHOUT a user + authKey, err := scenario.CreatePreAuthKeyWithOptions(hsic.AuthKeyOptions{ + User: nil, + Reusable: false, + Ephemeral: false, + Tags: []string{"tag:valid-owned"}, + }) + require.NoError(t, err) + t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.GetAclTags()) + + // Create a tailscale client WITHOUT --advertise-tags + client, err := scenario.CreateTailscaleNode( + "head", + tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]), + // Note: NO WithExtraLoginArgs for --advertise-tags + ) + require.NoError(t, err) + + // Login with the tags-only auth key + err = client.Login(headscale.GetEndpoint(), authKey.GetKey()) + require.NoError(t, err) + + // Wait for node to be registered and verify it has the key's tags + assert.EventuallyWithT(t, func(c *assert.CollectT) { + nodes, err := headscale.ListNodes(tagTestUser) + assert.NoError(c, err) + assert.Len(c, nodes, 1, "Should have exactly 1 node") + + if len(nodes) == 1 { + node := nodes[0] + t.Logf("Node registered with tags: %v", node.GetValidTags()) + assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"}) + } + }, 30*time.Second, 500*time.Millisecond, "verifying node inherited tags from auth key") + + t.Logf("Test 5.1 PASS: Node inherited tags from tags-only auth key") +} + +// TestTagsAuthKeyWithoutUserIgnoresAdvertisedTags tests that when an auth key without +// a user (tags-only) is used WITH --advertise-tags, the advertised tags are ignored +// and the auth key's tags are used instead. +// +// Test 5.2: Auth key without user, with --advertise-tags (should be ignored) +// Setup: Run `tailscale up --advertise-tags="tag:second" --auth-key AUTH_KEY_WITH_TAGS_NO_USER` +// Expected: Node registers with the auth key's tags (tag:valid-owned), NOT the advertised tags. +func TestTagsAuthKeyWithoutUserIgnoresAdvertisedTags(t *testing.T) { + IntegrationSkip(t) + + policy := tagsTestPolicy() + + spec := ScenarioSpec{ + NodesPerUser: 0, + Users: []string{tagTestUser}, + } + + scenario, err := NewScenario(spec) + + require.NoError(t, err) + defer scenario.ShutdownAssertNoPanics(t) + + err = scenario.CreateHeadscaleEnv( + []tsic.Option{}, + hsic.WithACLPolicy(policy), + hsic.WithTestName("tags-authkey-no-user-ignore-advertise"), + hsic.WithTLS(), + ) + requireNoErrHeadscaleEnv(t, err) + + headscale, err := scenario.Headscale() + requireNoErrGetHeadscale(t, err) + + // Create an auth key with tags but WITHOUT a user + authKey, err := scenario.CreatePreAuthKeyWithOptions(hsic.AuthKeyOptions{ + User: nil, + Reusable: false, + Ephemeral: false, + Tags: []string{"tag:valid-owned"}, + }) + require.NoError(t, err) + t.Logf("Created tags-only PreAuthKey with tags: %v", authKey.GetAclTags()) + + // Create a tailscale client WITH --advertise-tags for a DIFFERENT tag + client, err := scenario.CreateTailscaleNode( + "head", + tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]), + tsic.WithExtraLoginArgs([]string{"--advertise-tags=tag:second"}), + ) + require.NoError(t, err) + + // Login with the tags-only auth key + err = client.Login(headscale.GetEndpoint(), authKey.GetKey()) + require.NoError(t, err) + + // Wait for node to be registered and verify it has the auth KEY's tags, NOT the advertised tags + assert.EventuallyWithT(t, func(c *assert.CollectT) { + nodes, err := headscale.ListNodes(tagTestUser) + assert.NoError(c, err) + assert.Len(c, nodes, 1, "Should have exactly 1 node") + + if len(nodes) == 1 { + node := nodes[0] + t.Logf("Node registered with tags: %v (advertised: tag:second)", node.GetValidTags()) + // Should have auth key's tags, NOT the advertised tags + assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"}) + } + }, 30*time.Second, 500*time.Millisecond, "verifying node has auth key tags, not advertised tags") + + t.Logf("Test 5.2 PASS: Advertised tags were correctly ignored, auth key tags used") +}