Replace VNC JWT auth with a Noise_IK handshake bound to ACL-pushed pubkeys

This commit is contained in:
Viktor Liu
2026-05-21 17:36:15 +02:00
parent 9fd977c000
commit c0a3a2ee6d
36 changed files with 2014 additions and 1118 deletions
@@ -517,6 +517,9 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
if protocol == types.PolicyRuleProtocolNetbirdSSH || protocol == types.PolicyRuleProtocolNetbirdVNC {
policy.Rules[0].AuthorizedUser = userAuth.UserId
}
if protocol == types.PolicyRuleProtocolNetbirdVNC && req.SessionPubKey != nil {
policy.Rules[0].SessionPubKey = *req.SessionPubKey
}
_, err = h.accountManager.SavePolicy(r.Context(), userAuth.AccountId, userAuth.UserId, policy, true)
if err != nil {
@@ -526,9 +529,10 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
}
resp := &api.PeerTemporaryAccessResponse{
Id: peer.ID,
Name: peer.Name,
Rules: req.Rules,
Id: peer.ID,
Name: peer.Name,
Rules: req.Rules,
TargetPubKey: targetPeer.Key,
}
util.WriteJSONObject(r.Context(), w, resp)
+16 -16
View File
@@ -246,14 +246,14 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
t.Run("check that all peers get map", func(t *testing.T) {
for _, p := range account.Peers {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), p, validatedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), p, validatedPeers, account.GetActiveGroupUsers())
assert.GreaterOrEqual(t, len(peers), 1, "minimum number peers should present")
assert.GreaterOrEqual(t, len(firewallRules), 1, "minimum number of firewall rules should present")
}
})
t.Run("check first peer map details", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 8)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
@@ -509,7 +509,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
})
t.Run("check port ranges support for older peers", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 1)
assert.Contains(t, peers, account.Peers["peerI"])
@@ -635,7 +635,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
}
t.Run("check first peer map", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
assert.Contains(t, peers, account.Peers["peerC"])
expectedFirewallRules := []*types.FirewallRule{
@@ -665,7 +665,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
})
t.Run("check second peer map", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
assert.Contains(t, peers, account.Peers["peerB"])
expectedFirewallRules := []*types.FirewallRule{
@@ -697,7 +697,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
account.Policies[1].Rules[0].Bidirectional = false
t.Run("check first peer map directional only", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
assert.Contains(t, peers, account.Peers["peerC"])
expectedFirewallRules := []*types.FirewallRule{
@@ -719,7 +719,7 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
})
t.Run("check second peer map directional only", func(t *testing.T) {
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
assert.Contains(t, peers, account.Peers["peerB"])
expectedFirewallRules := []*types.FirewallRule{
@@ -917,7 +917,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
t.Run("verify peer's network map with default group peer list", func(t *testing.T) {
// peerB doesn't fulfill the NB posture check but is included in the destination group Swarm,
// will establish a connection with all source peers satisfying the NB posture check.
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
@@ -927,7 +927,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, 7)
expectedFirewallRules := []*types.FirewallRule{
@@ -992,7 +992,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
@@ -1002,7 +1002,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
// peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
@@ -1017,19 +1017,19 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
// peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers))
@@ -1044,14 +1044,14 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 3)
assert.Len(t, firewallRules, 3)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
peers, firewallRules, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers, account.GetActiveGroupUsers())
peers, firewallRules, _, _, _, _ = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers, account.GetActiveGroupUsers())
assert.Len(t, peers, 5)
// assert peers from Group Swarm
assert.Contains(t, peers, account.Peers["peerD"])
+2 -2
View File
@@ -849,7 +849,7 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) map
// GetPeerConnectionResources for a given peer
//
// This function returns the list of peers and firewall rules that are applicable to a given peer.
func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}, groupIDToUserIDs map[string][]string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, map[string]map[string]struct{}, bool) {
func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}, groupIDToUserIDs map[string][]string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, map[string]map[string]struct{}, []VNCSessionPubKey, bool) {
generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx, peer)
ctxState := &peerConnResolveState{
authorizedUsers: make(map[string]map[string]struct{}),
@@ -869,7 +869,7 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
}
peers, fwRules := getAccumulatedResources()
return peers, fwRules, ctxState.authorizedUsers, ctxState.vncAuthorizedUsers, ctxState.sshEnabled
return peers, fwRules, ctxState.authorizedUsers, ctxState.vncAuthorizedUsers, ctxState.vncSessionPubKeys, ctxState.sshEnabled
}
func (a *Account) applyPolicyRule(
+1
View File
@@ -49,6 +49,7 @@ type NetworkMap struct {
ForwardingRules []*ForwardingRule
AuthorizedUsers map[string]map[string]struct{}
VNCAuthorizedUsers map[string]map[string]struct{}
VNCSessionPubKeys []VNCSessionPubKey
EnableSSH bool
}
@@ -167,6 +167,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
RoutesFirewallRules: append(networkResourcesFirewallRules, routesFirewallRules...),
AuthorizedUsers: connRes.authorizedUsers,
VNCAuthorizedUsers: connRes.vncAuthorizedUsers,
VNCSessionPubKeys: connRes.vncSessionPubKeys,
EnableSSH: connRes.sshEnabled,
}
}
@@ -177,6 +178,7 @@ type peerConnectionResult struct {
firewallRules []*FirewallRule
authorizedUsers map[string]map[string]struct{}
vncAuthorizedUsers map[string]map[string]struct{}
vncSessionPubKeys []VNCSessionPubKey
sshEnabled bool
}
@@ -210,6 +212,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) p
firewallRules: fwRules,
authorizedUsers: state.authorizedUsers,
vncAuthorizedUsers: state.vncAuthorizedUsers,
vncSessionPubKeys: state.vncSessionPubKeys,
sshEnabled: state.sshEnabled,
}
}
@@ -15,9 +15,21 @@ import (
type peerConnResolveState struct {
authorizedUsers map[string]map[string]struct{}
vncAuthorizedUsers map[string]map[string]struct{}
vncSessionPubKeys []VNCSessionPubKey
sshEnabled bool
}
// VNCSessionPubKey carries an ephemeral X25519 static public key the
// dashboard registered via temporary-access. The daemon uses it as the
// allowed-client side of a Noise_IK handshake; a successful handshake
// authenticates the connection as UserID.
type VNCSessionPubKey struct {
// PubKey is the base64-encoded 32-byte X25519 public key.
PubKey string
// UserID is the unhashed user identity the pubkey authenticates as.
UserID string
}
// ruleAuthCallbacks lets Account and NetworkMapComponents share the per-rule
// direction-and-auth logic while keeping their own context/state plumbing for
// authorized-user collection and allowed-user lookups.
@@ -57,6 +69,12 @@ func applyResolvedRuleToState(
return
}
cb.collectVNCUsers(rule, state.vncAuthorizedUsers)
if rule.SessionPubKey != "" && rule.AuthorizedUser != "" {
state.vncSessionPubKeys = append(state.vncSessionPubKeys, VNCSessionPubKey{
PubKey: rule.SessionPubKey,
UserID: rule.AuthorizedUser,
})
}
case policyRuleImpliesLegacySSH(rule) && targetPeerSSHEnabled:
if !peerInDestinations {
return
+9 -1
View File
@@ -88,6 +88,12 @@ type PolicyRule struct {
// AuthorizedUser is a list of userIDs that are authorized to access local resources via ssh
AuthorizedUser string
// SessionPubKey is the base64 Ed25519 public key the AuthorizedUser
// will sign session-binding challenges with. Set together with
// AuthorizedUser when the rule was created via temporary-access for
// a VNC scope; empty otherwise.
SessionPubKey string
}
// Copy returns a copy of a policy rule
@@ -109,6 +115,7 @@ func (pm *PolicyRule) Copy() *PolicyRule {
PortRanges: make([]RulePortRange, len(pm.PortRanges)),
AuthorizedGroups: make(map[string][]string, len(pm.AuthorizedGroups)),
AuthorizedUser: pm.AuthorizedUser,
SessionPubKey: pm.SessionPubKey,
}
copy(rule.Destinations, pm.Destinations)
copy(rule.Sources, pm.Sources)
@@ -136,7 +143,8 @@ func (pm *PolicyRule) Equal(other *PolicyRule) bool {
pm.Protocol != other.Protocol ||
pm.SourceResource != other.SourceResource ||
pm.DestinationResource != other.DestinationResource ||
pm.AuthorizedUser != other.AuthorizedUser {
pm.AuthorizedUser != other.AuthorizedUser ||
pm.SessionPubKey != other.SessionPubKey {
return false
}