diff --git a/client/server/ssh_gate.go b/client/server/ssh_gate.go index ca1b4c4ee..d1bce25de 100644 --- a/client/server/ssh_gate.go +++ b/client/server/ssh_gate.go @@ -26,40 +26,51 @@ import ( // daemon's SSH server into a root (or unauthenticated) shell. // - Enabling the SSH server at all is what makes the above reachable, and a // profile the caller owns is not a privilege they hold. -// - While the SSH server is enabled, repointing the profile at another -// management identity hands SSH authorization decisions, including which -// keys and users are accepted, to whoever controls that identity. Changing -// the management URL and deregistering the peer are both ways to do that. +// - Enabling the VNC server exposes the console session, which on a +// multi-user host belongs to another user, and disabling its approval +// prompt removes that user's only say in it. +// - While a remote-access server (SSH or VNC) is enabled, repointing the +// profile at another management identity hands authorization decisions, +// including which keys and users are accepted, to whoever controls that +// identity. Changing the management URL and deregistering the peer are both +// ways to do that. // // Everything else stays unauthenticated, so this is not an authorization model: -// it only refuses the changes that would let a local user become root. A caller -// whose identity cannot be established is refused as well. +// it only refuses the changes that would let a local user become root, or reach +// another user's desktop. A caller whose identity cannot be established is +// refused as well. // privilegedConfigChange is the subset of a config request that crosses the // user-to-root boundary. Fields are nil or empty when the request leaves them // untouched. type privilegedConfigChange struct { - managementURL string - serverSSHAllowed *bool - enableSSHRoot *bool - disableSSHAuth *bool + managementURL string + serverSSHAllowed *bool + enableSSHRoot *bool + disableSSHAuth *bool + serverVNCAllowed *bool + disableVNCApproval *bool } func privilegedChangeFromSetConfig(msg *proto.SetConfigRequest) privilegedConfigChange { return privilegedConfigChange{ - managementURL: msg.GetManagementUrl(), - serverSSHAllowed: msg.ServerSSHAllowed, - enableSSHRoot: msg.EnableSSHRoot, - disableSSHAuth: msg.DisableSSHAuth, + managementURL: msg.GetManagementUrl(), + serverSSHAllowed: msg.ServerSSHAllowed, + enableSSHRoot: msg.EnableSSHRoot, + disableSSHAuth: msg.DisableSSHAuth, + serverVNCAllowed: msg.ServerVNCAllowed, + disableVNCApproval: msg.DisableVNCApproval, } } func privilegedChangeFromLogin(msg *proto.LoginRequest) privilegedConfigChange { return privilegedConfigChange{ - managementURL: msg.GetManagementUrl(), - serverSSHAllowed: msg.ServerSSHAllowed, - enableSSHRoot: msg.EnableSSHRoot, - disableSSHAuth: msg.DisableSSHAuth, + managementURL: msg.GetManagementUrl(), + serverSSHAllowed: msg.ServerSSHAllowed, + enableSSHRoot: msg.EnableSSHRoot, + disableSSHAuth: msg.DisableSSHAuth, + serverVNCAllowed: msg.ServerVNCAllowed, + disableVNCApproval: msg.DisableVNCApproval, } } @@ -83,15 +94,21 @@ func requirePrivilegeForConfigChange(ctx context.Context, stored *profilemanager return denyPrivileged(ctx, "enabling the NetBird SSH server", ipcauth.UpCommand("--allow-server-ssh")) } - // Only guard the management binding while the SSH server is enabled: that is - // when the management identity decides who may open a shell here. - if !sshServerEnabled(stored) { + if err := requirePrivilegeForVNCChange(ctx, stored, change); err != nil { + return err + } + + // Only guard the management binding while a remote-access server is enabled: + // that is when the management identity decides who may open a shell or reach + // the desktop here. + server, enabled := enabledRemoteAccessServer(stored) + if !enabled { return nil } if change.managementURL != "" && !sameManagementURL(stored.ManagementURL, change.managementURL) { return denyPrivileged(ctx, - "changing the management URL while the NetBird SSH server is enabled", + fmt.Sprintf("changing the management URL while the NetBird %s server is enabled", server), ipcauth.UpCommand("-m "+change.managementURL)) } @@ -99,20 +116,21 @@ func requirePrivilegeForConfigChange(ctx context.Context, stored *profilemanager } // requirePrivilegeForDeregistration refuses to deregister the peer from the -// management server when the caller is not privileged and the profile has the -// SSH server enabled. Deregistering frees the peer's key to be registered -// against another management identity, which is the same handover the +// management server when the caller is not privileged and the profile has a +// remote-access server enabled. Deregistering frees the peer's key to be +// registered against another management identity, which is the same handover the // management URL check refuses. // // Callers that treat deregistration as best-effort (profile removal) continue // without it; callers that were asked to deregister surface the error. func requirePrivilegeForDeregistration(ctx context.Context, cfg *profilemanager.Config) error { - if !sshServerEnabled(cfg) { + server, enabled := enabledRemoteAccessServer(cfg) + if !enabled { return nil } return denyPrivileged(ctx, - "deregistering this peer while the NetBird SSH server is enabled", + fmt.Sprintf("deregistering this peer while the NetBird %s server is enabled", server), ipcauth.ElevatedCommand("netbird logout")) } diff --git a/client/server/vnc_gate.go b/client/server/vnc_gate.go new file mode 100644 index 000000000..123e3c14d --- /dev/null +++ b/client/server/vnc_gate.go @@ -0,0 +1,58 @@ +package server + +import ( + "context" + + "github.com/netbirdio/netbird/client/internal/ipcauth" + "github.com/netbirdio/netbird/client/internal/profilemanager" +) + +// The VNC half of the privilege gate described in ssh_gate.go. The daemon runs +// as root/LocalSystem and the VNC server it hosts attaches to the console +// session, so both of these cross the user-to-root boundary: +// +// - Enabling the VNC server publishes the console desktop, keyboard and +// mouse to whoever the account authorizes. On a multi-user host that +// desktop belongs to another user, and the profile the caller owns is not a +// privilege they hold over it. +// - Disabling the approval prompt removes the console user's per-connection +// consent, turning an authorized VNC session into a silent one. +func requirePrivilegeForVNCChange(ctx context.Context, stored *profilemanager.Config, change privilegedConfigChange) error { + if enables(storedFlag(stored, func(c *profilemanager.Config) *bool { return c.DisableVNCApproval }), change.disableVNCApproval) { + return denyPrivileged(ctx, "disabling VNC connection approval", ipcauth.UpCommand("--disable-vnc-approval")) + } + + if enables(storedFlag(stored, func(c *profilemanager.Config) *bool { return c.ServerVNCAllowed }), change.serverVNCAllowed) { + return denyPrivileged(ctx, "enabling the NetBird VNC server", ipcauth.UpCommand("--allow-server-vnc")) + } + + return nil +} + +// vncServerEnabled reports whether the profile currently runs the VNC server. +// +// Unlike the SSH server, VNC defaults to off: the flag was introduced with the +// server itself, so a nil value is a config written before VNC existed and +// means the server is not running. +func vncServerEnabled(cfg *profilemanager.Config) bool { + if cfg == nil || cfg.ServerVNCAllowed == nil { + return false + } + return *cfg.ServerVNCAllowed +} + +// enabledRemoteAccessServer names a remote-access server the profile currently +// runs, and whether it runs any. It decides whether the management-binding and +// deregistration guards apply, since either server hands authorization +// decisions to the management identity the profile points at. SSH is reported +// first when both are on: it is the more privileged of the two. +func enabledRemoteAccessServer(cfg *profilemanager.Config) (string, bool) { + switch { + case sshServerEnabled(cfg): + return "SSH", true + case vncServerEnabled(cfg): + return "VNC", true + default: + return "", false + } +} diff --git a/client/server/vnc_gate_test.go b/client/server/vnc_gate_test.go new file mode 100644 index 000000000..e4a8db5f4 --- /dev/null +++ b/client/server/vnc_gate_test.go @@ -0,0 +1,178 @@ +package server + +import ( + "testing" + + "github.com/netbirdio/netbird/client/internal/profilemanager" +) + +func TestRequirePrivilegeForConfigChange_VNCFlags(t *testing.T) { + tests := []struct { + name string + stored *profilemanager.Config + change privilegedConfigChange + privileged bool + wantDeny bool + }{ + { + name: "enabling the vnc server unprivileged is refused", + stored: &profilemanager.Config{ServerVNCAllowed: boolPtr(false)}, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(true)}, + wantDeny: true, + }, + { + name: "enabling the vnc server as root is allowed", + stored: &profilemanager.Config{ServerVNCAllowed: boolPtr(false)}, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(true)}, + privileged: true, + }, + { + name: "restating an already enabled vnc server is not a change", + stored: &profilemanager.Config{ServerVNCAllowed: boolPtr(true)}, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(true)}, + }, + { + name: "turning the vnc server off is not guarded", + stored: &profilemanager.Config{ServerVNCAllowed: boolPtr(true)}, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(false)}, + }, + { + // Unlike SSH, a nil flag means off: the flag shipped with the server. + name: "a config written before vnc existed counts as off, so enabling is refused", + stored: &profilemanager.Config{}, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(true)}, + wantDeny: true, + }, + { + name: "a profile with no config yet counts as off, so enabling is refused", + stored: nil, + change: privilegedConfigChange{serverVNCAllowed: boolPtr(true)}, + wantDeny: true, + }, + { + name: "disabling the vnc approval prompt unprivileged is refused", + stored: &profilemanager.Config{DisableVNCApproval: boolPtr(false)}, + change: privilegedConfigChange{disableVNCApproval: boolPtr(true)}, + wantDeny: true, + }, + { + name: "disabling the vnc approval prompt as root is allowed", + stored: &profilemanager.Config{DisableVNCApproval: boolPtr(false)}, + change: privilegedConfigChange{disableVNCApproval: boolPtr(true)}, + privileged: true, + }, + { + name: "re-enabling the vnc approval prompt is not guarded", + stored: &profilemanager.Config{DisableVNCApproval: boolPtr(true)}, + change: privilegedConfigChange{disableVNCApproval: boolPtr(false)}, + }, + { + name: "restating a disabled approval prompt is not a change", + stored: &profilemanager.Config{DisableVNCApproval: boolPtr(true)}, + change: privilegedConfigChange{disableVNCApproval: boolPtr(true)}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := userCtx() + if tt.privileged { + ctx = rootCtx() + } + err := requirePrivilegeForConfigChange(ctx, tt.stored, tt.change) + if tt.wantDeny { + assertDenied(t, err) + return + } + assertAllowed(t, err) + }) + } +} + +// The management binding and deregistration guards protect either remote-access +// server, so the VNC server alone must arm them even with SSH off. +func TestRequirePrivilegeForConfigChange_ManagementURLWithVNCOnly(t *testing.T) { + vncOnly := &profilemanager.Config{ + ServerSSHAllowed: boolPtr(false), + ServerVNCAllowed: boolPtr(true), + ManagementURL: mustURL(t, "https://api.netbird.io:443"), + DisableVNCApproval: boolPtr(false), + } + + err := requirePrivilegeForConfigChange(userCtx(), vncOnly, + privilegedConfigChange{managementURL: "https://attacker.example.com:443"}) + assertDenied(t, err) + + // The same move is the administrator's to make. + assertAllowed(t, requirePrivilegeForConfigChange(rootCtx(), vncOnly, + privilegedConfigChange{managementURL: "https://selfhosted.example.com:443"})) + + // Restating the stored binding is not a change, so it is never refused. + assertAllowed(t, requirePrivilegeForConfigChange(userCtx(), vncOnly, + privilegedConfigChange{managementURL: "https://api.netbird.io"})) +} + +func TestRequirePrivilegeForDeregistration_VNCOnly(t *testing.T) { + vncOnly := &profilemanager.Config{ServerSSHAllowed: boolPtr(false), ServerVNCAllowed: boolPtr(true)} + + assertDenied(t, requirePrivilegeForDeregistration(userCtx(), vncOnly)) + assertAllowed(t, requirePrivilegeForDeregistration(rootCtx(), vncOnly)) + + bothOff := &profilemanager.Config{ServerSSHAllowed: boolPtr(false), ServerVNCAllowed: boolPtr(false)} + assertAllowed(t, requirePrivilegeForDeregistration(userCtx(), bothOff)) +} + +// enabledRemoteAccessServer names the server in the refusal, so the user is told +// which one is holding the binding down. SSH wins when both are on: it is the +// more privileged of the two. +func TestEnabledRemoteAccessServer(t *testing.T) { + tests := []struct { + name string + cfg *profilemanager.Config + wantServer string + wantOn bool + }{ + { + name: "ssh only", + cfg: &profilemanager.Config{ServerSSHAllowed: boolPtr(true), ServerVNCAllowed: boolPtr(false)}, + wantServer: "SSH", + wantOn: true, + }, + { + name: "vnc only", + cfg: &profilemanager.Config{ServerSSHAllowed: boolPtr(false), ServerVNCAllowed: boolPtr(true)}, + wantServer: "VNC", + wantOn: true, + }, + { + name: "both on reports ssh", + cfg: &profilemanager.Config{ServerSSHAllowed: boolPtr(true), ServerVNCAllowed: boolPtr(true)}, + wantServer: "SSH", + wantOn: true, + }, + { + name: "both off", + cfg: &profilemanager.Config{ServerSSHAllowed: boolPtr(false), ServerVNCAllowed: boolPtr(false)}, + }, + { + // A nil SSH flag means on (legacy configs), so it still arms the guard. + name: "legacy config with no flags at all reports ssh", + cfg: &profilemanager.Config{}, + wantServer: "SSH", + wantOn: true, + }, + { + name: "no config", + cfg: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, on := enabledRemoteAccessServer(tt.cfg) + if on != tt.wantOn || server != tt.wantServer { + t.Fatalf("enabledRemoteAccessServer() = (%q, %v), want (%q, %v)", server, on, tt.wantServer, tt.wantOn) + } + }) + } +} diff --git a/client/ui/frontend/src/modules/settings/PrivilegeGuard.tsx b/client/ui/frontend/src/modules/settings/PrivilegeGuard.tsx new file mode 100644 index 000000000..2f99eba28 --- /dev/null +++ b/client/ui/frontend/src/modules/settings/PrivilegeGuard.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from "react-i18next"; +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import { usePrivilege } from "@/hooks/usePrivilege.ts"; +import { Privilege } from "@bindings/services/models.js"; +import { type ReactNode } from "react"; + +export type GuardedControl = { + disabled: boolean; + hint: ReactNode; +}; + +// useGuardedControl returns a guard for the settings controls the daemon +// restricts to root/administrator: enabling a remote-access server, or removing +// one of its safeguards. +// +// The daemon restricts only the direction that hands out access from a process +// running as root. So for an unprivileged user a guarded control is either +// unavailable (it is off and only they could turn it on) or a one-way switch (it +// is on, they may turn it off, but not back on) — say which, either way. +// +// A null privilege means we could not determine it: leave the control alone +// rather than greying it out with nothing to explain why. The daemon enforces +// this regardless, and a rejected save reports its own guidance. +export const useGuardedControl = () => { + const privilege = usePrivilege(); + + return ( + guardedDirectionActive: boolean, + command: (p: Privilege) => string, + // inverted marks a control whose guarded direction is switching it off, + // so the one-way warning has to read the other way round. + inverted = false, + ): GuardedControl => { + if (!privilege || privilege.privileged) { + return { disabled: false, hint: undefined }; + } + const hint = ( + + ); + return { disabled: !guardedDirectionActive, hint }; + }; +}; + +// PrivilegeHint explains what an unprivileged user can and cannot do with a +// guarded control, and offers the command that does it with the privileges the +// daemon requires. oneWay covers the control being in the guarded state already: +// switching it back is the part that needs privileges. +export function PrivilegeHint({ + actor, + command, + oneWay, + inverted, +}: { + actor: string; + command: string; + oneWay: boolean; + inverted: boolean; +}): ReactNode { + const { t } = useTranslation(); + if (!command) return null; + return ( +
+ + {!oneWay + ? t("settings.privilege.hint", { actor }) + : inverted + ? t("settings.privilege.oneWayInverted", { actor }) + : t("settings.privilege.oneWay", { actor })} + + + + {command} + + +
+ ); +} diff --git a/client/ui/frontend/src/modules/settings/SettingsSSH.tsx b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx index bd91e520c..8c85ebb52 100644 --- a/client/ui/frontend/src/modules/settings/SettingsSSH.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx @@ -1,51 +1,19 @@ import { useTranslation } from "react-i18next"; -import { CopyToClipboard } from "@/components/CopyToClipboard"; import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; import { HelpText } from "@/components/typography/HelpText"; import { Input } from "@/components/inputs/Input"; import { Label } from "@/components/typography/Label"; import { cn } from "@/lib/cn"; import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useGuardedControl } from "@/modules/settings/PrivilegeGuard.tsx"; import { useSettings } from "@/contexts/SettingsContext.tsx"; -import { usePrivilege } from "@/hooks/usePrivilege.ts"; -import { Privilege } from "@bindings/services/models.js"; -import { type ChangeEvent, type ReactNode, useEffect, useId, useState } from "react"; +import { type ChangeEvent, useEffect, useId, useState } from "react"; export function SettingsSSH() { const { t } = useTranslation(); const { config, setField } = useSettings(); - const privilege = usePrivilege(); + const guarded = useGuardedControl(); const isSSHServerEnabled = config.serverSshAllowed; - - // The daemon restricts only the direction that hands out shells from a process - // running as root. So for an unprivileged user a guarded control is either - // unavailable (it is off and only they could turn it on) or a one-way switch - // (it is on, they may turn it off, but not back on) — say which, either way. - // - // A null privilege means we could not determine it: leave the control alone - // rather than greying it out with nothing to explain why. The daemon enforces - // this regardless, and a rejected save reports its own guidance. - const guarded = ( - guardedDirectionActive: boolean, - command: (p: Privilege) => string, - // inverted marks a control whose guarded direction is switching it off, so - // the one-way warning has to read the other way round. - inverted = false, - ) => { - if (!privilege || privilege.privileged) { - return { disabled: false, hint: undefined }; - } - const hint = ( - - ); - return { disabled: !guardedDirectionActive, hint }; - }; - const sshServer = guarded(config.serverSshAllowed, (p) => p.allowSshServer); const sshRoot = guarded(config.enableSshRoot, (p) => p.enableSshRoot); // Inverted control: the guarded direction is switching authentication off, so @@ -162,42 +130,3 @@ export function SettingsSSH() { ); } - -// PrivilegeHint explains what an unprivileged user can and cannot do with a -// guarded control, and offers the command that does it with the privileges the -// daemon requires. oneWay covers the control being in the guarded state already: -// switching it back is the part that needs privileges. -function PrivilegeHint({ - actor, - command, - oneWay, - inverted, -}: { - actor: string; - command: string; - oneWay: boolean; - inverted: boolean; -}): ReactNode { - const { t } = useTranslation(); - if (!command) return null; - return ( -
- - {!oneWay - ? t("settings.ssh.privilege.hint", { actor }) - : inverted - ? t("settings.ssh.privilege.oneWayInverted", { actor }) - : t("settings.ssh.privilege.oneWay", { actor })} - - - - {command} - - -
- ); -} diff --git a/client/ui/frontend/src/modules/settings/SettingsVNC.tsx b/client/ui/frontend/src/modules/settings/SettingsVNC.tsx index 78a6f3ad4..a5fa8c9fc 100644 --- a/client/ui/frontend/src/modules/settings/SettingsVNC.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsVNC.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useGuardedControl } from "@/modules/settings/PrivilegeGuard.tsx"; import { useSettings } from "@/contexts/SettingsContext.tsx"; import { useRestrictions } from "@/contexts/RestrictionsContext.tsx"; @@ -8,9 +9,15 @@ export function SettingsVNC() { const { t } = useTranslation(); const { config, setField } = useSettings(); const { mdm } = useRestrictions(); + const guarded = useGuardedControl(); const isVNCServerEnabled = config.serverVncAllowed; const vncServerManaged = mdm.allowServerVNC != null; + const vncServer = guarded(config.serverVncAllowed, (p) => p.allowVncServer); + // Inverted control: the guarded direction is switching the approval prompt + // off, so the already-disabled state is the one-way one. + const vncApproval = guarded(config.disableVncApproval, (p) => p.disableVncApproval, true); + return ( <> @@ -19,8 +26,9 @@ export function SettingsVNC() { onChange={(v) => setField("serverVncAllowed", v)} label={t("settings.vnc.server.label")} helpText={t("settings.vnc.server.help")} - disabled={vncServerManaged} + disabled={vncServerManaged || vncServer.disabled} /> + {!vncServerManaged && vncServer.hint} {!mdm.disableVNCApproval && ( @@ -33,7 +41,9 @@ export function SettingsVNC() { onChange={(v) => setField("disableVncApproval", !v)} label={t("settings.vnc.approval.label")} helpText={t("settings.vnc.approval.help")} + disabled={vncApproval.disabled} /> + {vncApproval.hint} )} diff --git a/client/ui/i18n/locales/en/common.json b/client/ui/i18n/locales/en/common.json index a1811baae..57461e2a9 100644 --- a/client/ui/i18n/locales/en/common.json +++ b/client/ui/i18n/locales/en/common.json @@ -1855,16 +1855,16 @@ "message": "Operation failed.", "description": "Generic fallback error message used when no specific error applies." }, - "settings.ssh.privilege.hint": { + "settings.privilege.hint": { "message": "Requires {actor}. Run this instead:", - "description": "Help text under an SSH setting the user cannot change: it needs elevated privileges. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." + "description": "Help text under a remote-access setting the user cannot change: it needs elevated privileges. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." }, - "settings.ssh.privilege.oneWay": { + "settings.privilege.oneWay": { "message": "You can switch this off, but switching it back on needs {actor}:", - "description": "Warning under an SSH setting an unprivileged user may disable but not re-enable. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." + "description": "Warning under a remote-access setting an unprivileged user may disable but not re-enable. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." }, - "settings.ssh.privilege.oneWayInverted": { + "settings.privilege.oneWayInverted": { "message": "You can switch this on, but switching it back off needs {actor}:", - "description": "Warning under the SSH authentication setting, which an unprivileged user may re-enable but not disable again. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." + "description": "Warning under a safeguard setting (SSH authentication, VNC approval) which an unprivileged user may re-enable but not disable again. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command." } } diff --git a/client/ui/services/settings.go b/client/ui/services/settings.go index 0da83709c..e087cfec5 100644 --- a/client/ui/services/settings.go +++ b/client/ui/services/settings.go @@ -53,9 +53,11 @@ type Privilege struct { // Actor names what the operation requires ("root", "administrator privileges"). Actor string `json:"actor"` // Commands equivalent to the settings the daemon guards, ready to copy. - AllowSSHServer string `json:"allowSshServer"` - EnableSSHRoot string `json:"enableSshRoot"` - DisableSSHAuth string `json:"disableSshAuth"` + AllowSSHServer string `json:"allowSshServer"` + EnableSSHRoot string `json:"enableSshRoot"` + DisableSSHAuth string `json:"disableSshAuth"` + AllowVNCServer string `json:"allowVncServer"` + DisableVNCApproval string `json:"disableVncApproval"` } type ConfigParams struct { @@ -233,8 +235,8 @@ func (s *Settings) SetConfig(ctx context.Context, p SetConfigParams) error { } // Privilege reports whether this UI process could carry out the changes the -// daemon restricts to root/administrator, and the command that performs the one -// users hit in the SSH settings. It applies the daemon's own rule to what it can +// daemon restricts to root/administrator, and the command that performs each of +// the ones users hit in the SSH and VNC settings. It applies the daemon's own rule to what it can // see locally, so the frontend can present those controls as unavailable up front // instead of letting a save fail. No daemon round-trip, so it also works while the // daemon is down. @@ -259,11 +261,13 @@ func (s *Settings) Privilege() Privilege { func newPrivilege(privileged bool) Privilege { return Privilege{ - Privileged: privileged, - Actor: ipcauth.PrivilegedActor(), - AllowSSHServer: ipcauth.UpCommand("--allow-server-ssh"), - EnableSSHRoot: ipcauth.UpCommand("--enable-ssh-root"), - DisableSSHAuth: ipcauth.UpCommand("--disable-ssh-auth"), + Privileged: privileged, + Actor: ipcauth.PrivilegedActor(), + AllowSSHServer: ipcauth.UpCommand("--allow-server-ssh"), + EnableSSHRoot: ipcauth.UpCommand("--enable-ssh-root"), + DisableSSHAuth: ipcauth.UpCommand("--disable-ssh-auth"), + AllowVNCServer: ipcauth.UpCommand("--allow-server-vnc"), + DisableVNCApproval: ipcauth.UpCommand("--disable-vnc-approval"), } } diff --git a/docs/netbird-policy.reg b/docs/netbird-policy.reg index 7c5090942..f01ec276b 100644 Binary files a/docs/netbird-policy.reg and b/docs/netbird-policy.reg differ