From 58fcaaf9a53689a93f79d379834bf5d266d9438f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 9 Jul 2026 08:30:24 +0000 Subject: [PATCH] ipn/conf: support RemoteConfig in the config file Add ConfigVAlpha.RemoteConfig so a user-data/cloud-init config can delegate remote control to the tailnet admin (see Prefs.RemoteConfig). Updates #1866 Signed-off-by: Kristoffer Dalby --- gokrazy/README.md | 4 +++- ipn/conf.go | 5 +++++ ipn/conf_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/gokrazy/README.md b/gokrazy/README.md index f41d43cbf..711995181 100644 --- a/gokrazy/README.md +++ b/gokrazy/README.md @@ -92,7 +92,9 @@ single AMI supports two ways of joining a tailnet: ``` A config present in user-data locks the CLI (`tailscale set`/`up` are - rejected) unless it sets `"Locked": false`. + rejected) unless it sets `"Locked": false`. Add `"RemoteConfig": true` to + hand full remote management of the node to the tailnet admin (see + `Prefs.RemoteConfig`) — appropriate for admin-owned fleet devices. - **Interactive (serial console):** launch the AMI with *no* user-data. The `optional:` prefix means the missing config is not an error, so `tailscaled` diff --git a/ipn/conf.go b/ipn/conf.go index 266cf5592..df663766b 100644 --- a/ipn/conf.go +++ b/ipn/conf.go @@ -47,6 +47,7 @@ type ConfigVAlpha struct { RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH RunWebClient opt.Bool `json:",omitempty"` ShieldsUp opt.Bool `json:",omitempty"` + RemoteConfig opt.Bool `json:",omitzero"` // delegate full remote control to the tailnet admin; see Prefs.RemoteConfig AutoUpdate *AutoUpdatePrefs `json:",omitempty"` ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this @@ -167,6 +168,10 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) { mp.ShieldsUp = c.ShieldsUp.EqualBool(true) mp.ShieldsUpSet = true } + if c.RemoteConfig != "" { + mp.RemoteConfig = c.RemoteConfig.EqualBool(true) + mp.RemoteConfigSet = true + } if c.AutoUpdate != nil { mp.AutoUpdate = *c.AutoUpdate mp.AutoUpdateSet = AutoUpdatePrefsMask{ApplySet: true, CheckSet: true} diff --git a/ipn/conf_test.go b/ipn/conf_test.go index c2f6f9fcf..b394e438e 100644 --- a/ipn/conf_test.go +++ b/ipn/conf_test.go @@ -186,3 +186,28 @@ func TestConfigVAlphaToPrefs(t *testing.T) { }) } } + +func TestConfigVAlphaToPrefsRemoteConfig(t *testing.T) { + for name, tt := range map[string]struct { + cfg ConfigVAlpha + wantSet bool + wantVal bool + }{ + "absent": {ConfigVAlpha{Version: "alpha0"}, false, false}, + "true": {ConfigVAlpha{Version: "alpha0", RemoteConfig: "true"}, true, true}, + "false": {ConfigVAlpha{Version: "alpha0", RemoteConfig: "false"}, true, false}, + } { + t.Run(name, func(t *testing.T) { + mp, err := tt.cfg.ToPrefs() + if err != nil { + t.Fatal(err) + } + if mp.RemoteConfigSet != tt.wantSet { + t.Errorf("RemoteConfigSet = %v; want %v", mp.RemoteConfigSet, tt.wantSet) + } + if mp.RemoteConfig != tt.wantVal { + t.Errorf("RemoteConfig = %v; want %v", mp.RemoteConfig, tt.wantVal) + } + }) + } +}