Skip to content

Commit 4fe5cbe

Browse files
authored
hscontrol/oidc: fix ACL policy not applied to new OIDC nodes (#2890)
Fixes #2888 Fixes #2896
1 parent 7e8cee6 commit 4fe5cbe

File tree

9 files changed

+757
-107
lines changed

9 files changed

+757
-107
lines changed

.github/workflows/test-integration.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
- TestAuthKeyLogoutAndReloginNewUser
3434
- TestAuthKeyLogoutAndReloginSameUserExpiredKey
3535
- TestAuthKeyDeleteKey
36+
- TestAuthKeyLogoutAndReloginRoutesPreserved
3637
- TestOIDCAuthenticationPingAll
3738
- TestOIDCExpireNodesBasedOnTokenExpiry
3839
- TestOIDC024UserCreation
@@ -42,6 +43,8 @@ jobs:
4243
- TestOIDCMultipleOpenedLoginUrls
4344
- TestOIDCReloginSameNodeSameUser
4445
- TestOIDCExpiryAfterRestart
46+
- TestOIDCACLPolicyOnJoin
47+
- TestOIDCReloginSameUserRoutesPreserved
4548
- TestAuthWebFlowAuthenticationPingAll
4649
- TestAuthWebFlowLogoutAndReloginSameUser
4750
- TestAuthWebFlowLogoutAndReloginNewUser

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
### Changes
66

7+
## 0.27.2 (2025-xx-xx)
8+
9+
### Changes
10+
11+
- Fix ACL policy not applied to new OIDC nodes until client restart
12+
[#2890](https://github.com/juanfont/headscale/pull/2890)
13+
714
## 0.27.1 (2025-11-11)
815

916
**Minimum supported Tailscale client version: v1.64.0**

hscontrol/auth.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"time"
1212

1313
"github.com/juanfont/headscale/hscontrol/types"
14-
"github.com/juanfont/headscale/hscontrol/types/change"
1514
"github.com/juanfont/headscale/hscontrol/util"
1615
"github.com/rs/zerolog/log"
1716
"gorm.io/gorm"
@@ -364,16 +363,13 @@ func (h *Headscale) handleRegisterWithAuthKey(
364363
// eventbus.
365364
// TODO(kradalby): This needs to be ran as part of the batcher maybe?
366365
// now since we dont update the node/pol here anymore
367-
routeChange := h.state.AutoApproveRoutes(node)
368-
369-
if _, _, err := h.state.SaveNode(node); err != nil {
370-
return nil, fmt.Errorf("saving auto approved routes to node: %w", err)
366+
routesChange, err := h.state.AutoApproveRoutes(node)
367+
if err != nil {
368+
return nil, fmt.Errorf("auto approving routes: %w", err)
371369
}
372370

373-
if routeChange && changed.Empty() {
374-
changed = change.NodeAdded(node.ID())
375-
}
376-
h.Change(changed)
371+
// Send both changes. Empty changes are ignored by Change().
372+
h.Change(changed, routesChange)
377373

378374
// TODO(kradalby): I think this is covered above, but we need to validate that.
379375
// // If policy changed due to node registration, send a separate policy change

hscontrol/grpcv1.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,13 @@ func (api headscaleV1APIServer) RegisterNode(
294294
// ensure we send an update.
295295
// This works, but might be another good candidate for doing some sort of
296296
// eventbus.
297-
_ = api.h.state.AutoApproveRoutes(node)
298-
_, _, err = api.h.state.SaveNode(node)
297+
routeChange, err := api.h.state.AutoApproveRoutes(node)
299298
if err != nil {
300-
return nil, fmt.Errorf("saving auto approved routes to node: %w", err)
299+
return nil, fmt.Errorf("auto approving routes: %w", err)
301300
}
302301

303-
api.h.Change(nodeChange)
302+
// Send both changes. Empty changes are ignored by Change().
303+
api.h.Change(nodeChange, routeChange)
304304

305305
return &v1.RegisterNodeResponse{Node: node.Proto()}, nil
306306
}

hscontrol/oidc.go

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ var (
4242
errOIDCAllowedUsers = errors.New(
4343
"authenticated principal does not match any allowed user",
4444
)
45-
errOIDCInvalidNodeState = errors.New(
46-
"requested node state key expired before authorisation completed",
47-
)
48-
errOIDCNodeKeyMissing = errors.New("could not get node key from cache")
4945
)
5046

5147
// RegistrationInfo contains both machine key and verifier information for OIDC validation.
@@ -108,16 +104,8 @@ func (a *AuthProviderOIDC) AuthURL(registrationID types.RegistrationID) string {
108104
registrationID.String())
109105
}
110106

111-
func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time.Time {
112-
if a.cfg.UseExpiryFromToken {
113-
return idTokenExpiration
114-
}
115-
116-
return time.Now().Add(a.cfg.Expiry)
117-
}
118-
119-
// RegisterOIDC redirects to the OIDC provider for authentication
120-
// Puts NodeKey in cache so the callback can retrieve it using the oidc state param
107+
// RegisterHandler registers the OIDC callback handler with the given router.
108+
// It puts NodeKey in cache so the callback can retrieve it using the oidc state param.
121109
// Listens in /register/:registration_id.
122110
func (a *AuthProviderOIDC) RegisterHandler(
123111
writer http.ResponseWriter,
@@ -304,7 +292,7 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
304292
return
305293
}
306294

307-
user, c, err := a.createOrUpdateUserFromClaim(&claims)
295+
user, _, err := a.createOrUpdateUserFromClaim(&claims)
308296
if err != nil {
309297
log.Error().
310298
Err(err).
@@ -323,9 +311,6 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
323311
return
324312
}
325313

326-
// Send policy update notifications if needed
327-
a.h.Change(c)
328-
329314
// TODO(kradalby): Is this comment right?
330315
// If the node exists, then the node should be reauthenticated,
331316
// if the node does not exist, and the machine key exists, then
@@ -372,6 +357,14 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
372357
httpError(writer, NewHTTPError(http.StatusGone, "login session expired, try again", nil))
373358
}
374359

360+
func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time.Time {
361+
if a.cfg.UseExpiryFromToken {
362+
return idTokenExpiration
363+
}
364+
365+
return time.Now().Add(a.cfg.Expiry)
366+
}
367+
375368
func extractCodeAndStateParamFromRequest(
376369
req *http.Request,
377370
) (string, string, error) {
@@ -504,8 +497,8 @@ func (a *AuthProviderOIDC) createOrUpdateUserFromClaim(
504497
}
505498

506499
// if the user is still not found, create a new empty user.
507-
// TODO(kradalby): This might cause us to not have an ID below which
508-
// is a problem.
500+
// TODO(kradalby): This context is not inherited from the request, which is probably not ideal.
501+
// However, we need a context to use the OIDC provider.
509502
if user == nil {
510503
newUser = true
511504
user = &types.User{}
@@ -557,18 +550,13 @@ func (a *AuthProviderOIDC) handleRegistration(
557550
// ensure we send an update.
558551
// This works, but might be another good candidate for doing some sort of
559552
// eventbus.
560-
_ = a.h.state.AutoApproveRoutes(node)
561-
_, policyChange, err := a.h.state.SaveNode(node)
553+
routesChange, err := a.h.state.AutoApproveRoutes(node)
562554
if err != nil {
563-
return false, fmt.Errorf("saving auto approved routes to node: %w", err)
555+
return false, fmt.Errorf("auto approving routes: %w", err)
564556
}
565557

566-
// Policy updates are full and take precedence over node changes.
567-
if !policyChange.Empty() {
568-
a.h.Change(policyChange)
569-
} else {
570-
a.h.Change(nodeChange)
571-
}
558+
// Send both changes. Empty changes are ignored by Change().
559+
a.h.Change(nodeChange, routesChange)
572560

573561
return !nodeChange.Empty(), nil
574562
}

hscontrol/state/state.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ func (s *State) SetPolicy(pol []byte) (bool, error) {
827827

828828
// AutoApproveRoutes checks if a node's routes should be auto-approved.
829829
// AutoApproveRoutes checks if any routes should be auto-approved for a node and updates them.
830-
func (s *State) AutoApproveRoutes(nv types.NodeView) bool {
830+
func (s *State) AutoApproveRoutes(nv types.NodeView) (change.ChangeSet, error) {
831831
approved, changed := policy.ApproveRoutesWithPolicy(s.polMan, nv, nv.ApprovedRoutes().AsSlice(), nv.AnnouncedRoutes())
832832
if changed {
833833
log.Debug().
@@ -840,21 +840,23 @@ func (s *State) AutoApproveRoutes(nv types.NodeView) bool {
840840

841841
// Persist the auto-approved routes to database and NodeStore via SetApprovedRoutes
842842
// This ensures consistency between database and NodeStore
843-
_, _, err := s.SetApprovedRoutes(nv.ID(), approved)
843+
_, c, err := s.SetApprovedRoutes(nv.ID(), approved)
844844
if err != nil {
845845
log.Error().
846846
Uint64("node.id", nv.ID().Uint64()).
847847
Str("node.name", nv.Hostname()).
848848
Err(err).
849849
Msg("Failed to persist auto-approved routes")
850850

851-
return false
851+
return change.EmptySet, err
852852
}
853853

854854
log.Info().Uint64("node.id", nv.ID().Uint64()).Str("node.name", nv.Hostname()).Strs("routes.approved", util.PrefixesToString(approved)).Msg("Routes approved")
855+
856+
return c, nil
855857
}
856858

857-
return changed
859+
return change.EmptySet, nil
858860
}
859861

860862
// GetPolicy retrieves the current policy from the database.
@@ -1637,6 +1639,7 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
16371639
var routeChange bool
16381640
var hostinfoChanged bool
16391641
var needsRouteApproval bool
1642+
var autoApprovedRoutes []netip.Prefix
16401643
// We need to ensure we update the node as it is in the NodeStore at
16411644
// the time of the request.
16421645
updatedNode, ok := s.nodeStore.UpdateNode(id, func(currentNode *types.Node) {
@@ -1661,7 +1664,6 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
16611664
}
16621665

16631666
// Calculate route approval before NodeStore update to avoid calling View() inside callback
1664-
var autoApprovedRoutes []netip.Prefix
16651667
var hasNewRoutes bool
16661668
if hi := req.Hostinfo; hi != nil {
16671669
hasNewRoutes = len(hi.RoutableIPs) > 0
@@ -1727,7 +1729,6 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
17271729
Strs("newApprovedRoutes", util.PrefixesToString(autoApprovedRoutes)).
17281730
Bool("routeChanged", routeChange).
17291731
Msg("applying route approval results")
1730-
currentNode.ApprovedRoutes = autoApprovedRoutes
17311732
}
17321733
}
17331734
})
@@ -1736,6 +1737,24 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
17361737
return change.EmptySet, fmt.Errorf("node not found in NodeStore: %d", id)
17371738
}
17381739

1740+
if routeChange {
1741+
log.Debug().
1742+
Uint64("node.id", id.Uint64()).
1743+
Strs("autoApprovedRoutes", util.PrefixesToString(autoApprovedRoutes)).
1744+
Msg("Persisting auto-approved routes from MapRequest")
1745+
1746+
// SetApprovedRoutes will update both database and PrimaryRoutes table
1747+
_, c, err := s.SetApprovedRoutes(id, autoApprovedRoutes)
1748+
if err != nil {
1749+
return change.EmptySet, fmt.Errorf("persisting auto-approved routes: %w", err)
1750+
}
1751+
1752+
// If SetApprovedRoutes resulted in a policy change, return it
1753+
if !c.Empty() {
1754+
return c, nil
1755+
}
1756+
} // Continue with the rest of the processing using the updated node
1757+
17391758
nodeRouteChange := change.EmptySet
17401759

17411760
// Handle route changes after NodeStore update
@@ -1750,13 +1769,8 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
17501769
routesChangedButNotApproved = true
17511770
}
17521771
}
1753-
if routeChange {
1754-
needsRouteUpdate = true
1755-
log.Debug().
1756-
Caller().
1757-
Uint64("node.id", id.Uint64()).
1758-
Msg("updating routes because approved routes changed")
1759-
} else if routesChangedButNotApproved {
1772+
1773+
if routesChangedButNotApproved {
17601774
needsRouteUpdate = true
17611775
log.Debug().
17621776
Caller().
@@ -1793,25 +1807,26 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
17931807
return change.NodeAdded(id), nil
17941808
}
17951809

1796-
func hostinfoEqual(oldNode types.NodeView, new *tailcfg.Hostinfo) bool {
1797-
if !oldNode.Valid() && new == nil {
1810+
func hostinfoEqual(oldNode types.NodeView, newHI *tailcfg.Hostinfo) bool {
1811+
if !oldNode.Valid() && newHI == nil {
17981812
return true
17991813
}
1800-
if !oldNode.Valid() || new == nil {
1814+
1815+
if !oldNode.Valid() || newHI == nil {
18011816
return false
18021817
}
18031818
old := oldNode.AsStruct().Hostinfo
18041819

1805-
return old.Equal(new)
1820+
return old.Equal(newHI)
18061821
}
18071822

1808-
func routesChanged(oldNode types.NodeView, new *tailcfg.Hostinfo) bool {
1823+
func routesChanged(oldNode types.NodeView, newHI *tailcfg.Hostinfo) bool {
18091824
var oldRoutes []netip.Prefix
18101825
if oldNode.Valid() && oldNode.AsStruct().Hostinfo != nil {
18111826
oldRoutes = oldNode.AsStruct().Hostinfo.RoutableIPs
18121827
}
18131828

1814-
newRoutes := new.RoutableIPs
1829+
newRoutes := newHI.RoutableIPs
18151830
if newRoutes == nil {
18161831
newRoutes = []netip.Prefix{}
18171832
}

0 commit comments

Comments
 (0)