cmd/tailscale, ipn, feature/remoteconfig: add remote-config support

Add a new Prefs.RemoteConfig bool. When true, a c2n endpoint at
/remoteapi/localapi/* proxies into this node's LocalAPI at
/localapi/* with full read/write permission, giving the tailnet
admin the same API surface a local root/admin user has via the
tailscale CLI. All LocalAPI versions (v0, v1, ...) proxy through.

RemoteConfig is an alternative to Tailscale's default per-feature
double opt-in, in which both the tailnet admin and the local machine
owner must consent to each individual setting change. It is a single
client-side "I trust the tailnet admin" switch that, once on, hands
over full remote management of this node's settings and LocalAPI
without any further local prompt or confirmation.

This is only appropriate when the tailnet admin already owns the
machine (e.g. a corporate fleet device) or the local user has
explicitly delegated full control. It should never be enabled on a
personal/BYOD device with an untrusted tailnet admin. The trust
model is documented on the pref, on the hidden --remote-config CLI
flag, and on the feature/remoteconfig package.

The node advertises its RemoteConfig state to the control plane via
a new Hostinfo.RemoteConfig bool. This is only true when the feature
is both compiled in (buildfeatures.HasRemoteConfig) and its init
actually ran (feature.IsRegistered("remoteconfig")); tsnet builds
have the former but not the latter and correctly report false.

The handler lives in feature/remoteconfig and can be omitted with the
ts_omit_remoteconfig build tag. tsnet's TestDeps guards against
accidentally pulling it in.

Updates tailscale/corp#18043

Change-Id: I72ce10a90a0e4e738c72c940af3af64c986160b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-07 12:10:34 -07:00
committed by Brad Fitzpatrick
parent 2051c5f358
commit c1ae2bb1f8
23 changed files with 444 additions and 1 deletions
+2
View File
@@ -190,6 +190,8 @@ func prefValue(flagName string, prefs *ipn.Prefs, st *ipnstate.Status) any {
return prefs.ForceDaemon
case "sync":
return prefs.Sync.EqualBool(true)
case "remote-config":
return prefs.RemoteConfig
case "relay-server-port":
if prefs.RelayServerPort != nil {
return fmt.Sprint(*prefs.RelayServerPort)
+3
View File
@@ -62,6 +62,7 @@ type setArgsT struct {
updateCheck bool
updateApply bool
reportPosture bool
remoteConfig bool
snat bool
statefulFiltering bool
sync bool
@@ -88,6 +89,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
setf.BoolVar(&setArgs.reportPosture, "report-posture", false, "allow management plane to gather device posture information")
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
setf.BoolVar(&setArgs.remoteConfig, "remote-config", false, hidden+"delegate FULL remote control of this node's prefs and LocalAPI to the tailnet admin, bypassing Tailscale's per-feature double opt-in; only use when the tailnet admin owns or is fully trusted with this machine")
setf.BoolVar(&setArgs.sync, "sync", false, hidden+"actively sync configuration from the control plane (set to false only for network failure testing)")
setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "UDP port number (0 will pick a random unused port) for the relay server to bind to, on all interfaces, or empty string to disable relay server functionality")
setf.StringVar(&setArgs.relayServerStaticEndpoints, "relay-server-static-endpoints", "", "static IP:port endpoints to advertise as candidates for relay connections (comma-separated, e.g. \"[2001:db8::1]:40000,192.0.2.1:40000\") or empty string to not advertise any static endpoints")
@@ -163,6 +165,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
Advertise: setArgs.advertiseConnector,
},
PostureChecking: setArgs.reportPosture,
RemoteConfig: setArgs.remoteConfig,
NoStatefulFiltering: opt.NewBool(!setArgs.statefulFiltering),
},
}
+1
View File
@@ -917,6 +917,7 @@ func init() {
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
addPrefFlagMapping("advertise-connector", "AppConnector")
addPrefFlagMapping("report-posture", "PostureChecking")
addPrefFlagMapping("remote-config", "RemoteConfig")
addPrefFlagMapping("relay-server-port", "RelayServerPort")
addPrefFlagMapping("sync", "Sync")
addPrefFlagMapping("relay-server-static-endpoints", "RelayServerStaticEndpoints")
+1
View File
@@ -310,6 +310,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
tailscale.com/feature/posture from tailscale.com/feature/condregister
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
tailscale.com/feature/remoteconfig from tailscale.com/feature/condregister
tailscale.com/feature/routecheck from tailscale.com/feature/condregister
tailscale.com/feature/runtimemetrics from tailscale.com/feature/condregister
L tailscale.com/feature/sdnotify from tailscale.com/feature/condregister
+1
View File
@@ -195,6 +195,7 @@ OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.OperatorUserSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.PostureCheckingSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.ProfileNameSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.RelayServerPortSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.RemoteConfigSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.RouteAllSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.RunSSHSet
OmitEmptyUnsupportedInV2 tailscale.com/ipn.MaskedPrefs.RunWebClientSet