ipn, cmd/tailscale/cli: add pref to configure sudo-free operator user
From discussion with @danderson. Fixes #1684 (in a different way) Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>pull/1736/head
parent
3739cf22b0
commit
8f3e453356
|
@ -23,6 +23,7 @@ import (
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/safesocket"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/preftype"
|
"tailscale.com/types/preftype"
|
||||||
|
@ -69,6 +70,9 @@ var upFlagSet = (func() *flag.FlagSet {
|
||||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
|
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
|
||||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||||
|
if safesocket.PlatformUsesPeerCreds() {
|
||||||
|
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||||
|
}
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||||
|
@ -104,6 +108,7 @@ type upArgsT struct {
|
||||||
netfilterMode string
|
netfilterMode string
|
||||||
authKey string
|
authKey string
|
||||||
hostname string
|
hostname string
|
||||||
|
opUser string
|
||||||
}
|
}
|
||||||
|
|
||||||
var upArgs upArgsT
|
var upArgs upArgsT
|
||||||
|
@ -212,6 +217,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||||
prefs.NoSNAT = !upArgs.snat
|
prefs.NoSNAT = !upArgs.snat
|
||||||
prefs.Hostname = upArgs.hostname
|
prefs.Hostname = upArgs.hostname
|
||||||
prefs.ForceDaemon = upArgs.forceDaemon
|
prefs.ForceDaemon = upArgs.forceDaemon
|
||||||
|
prefs.OperatorUser = upArgs.opUser
|
||||||
|
|
||||||
if goos == "linux" {
|
if goos == "linux" {
|
||||||
switch upArgs.netfilterMode {
|
switch upArgs.netfilterMode {
|
||||||
|
@ -447,6 +453,7 @@ func init() {
|
||||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP")
|
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP")
|
||||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||||
|
addPrefFlagMapping("operator", "OperatorUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||||
|
@ -475,7 +482,7 @@ func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||||
case "advertise-exit-node":
|
case "advertise-exit-node":
|
||||||
// This pref is a shorthand for advertise-routes.
|
// This pref is a shorthand for advertise-routes.
|
||||||
default:
|
default:
|
||||||
panic("internal error: unhandled flag " + flagName)
|
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -2176,6 +2177,27 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||||
|
// os/user.User.Uid string form), or the empty string if none.
|
||||||
|
func (b *LocalBackend) OperatorUserID() string {
|
||||||
|
b.mu.Lock()
|
||||||
|
if b.prefs == nil {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
opUserName := b.prefs.OperatorUser
|
||||||
|
b.mu.Unlock()
|
||||||
|
if opUserName == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
u, err := user.Lookup(opUserName)
|
||||||
|
if err != nil {
|
||||||
|
b.logf("error looking up operator %q uid: %v", opUserName, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return u.Uid
|
||||||
|
}
|
||||||
|
|
||||||
// TestOnlyPublicKeys returns the current machine and node public
|
// TestOnlyPublicKeys returns the current machine and node public
|
||||||
// keys. Used in tests only to facilitate automated node authorization
|
// keys. Used in tests only to facilitate automated node authorization
|
||||||
// in the test harness.
|
// in the test harness.
|
||||||
|
|
|
@ -287,7 +287,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||||
defer s.removeAndCloseConn(c)
|
defer s.removeAndCloseConn(c)
|
||||||
logf("[v1] incoming control connection")
|
logf("[v1] incoming control connection")
|
||||||
|
|
||||||
if isReadonlyConn(ci, logf) {
|
if isReadonlyConn(ci, s.b.OperatorUserID(), logf) {
|
||||||
ctx = ipn.ReadonlyContextOf(ctx)
|
ctx = ipn.ReadonlyContextOf(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,7 +313,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// Windows doesn't need/use this mechanism, at least yet. It
|
// Windows doesn't need/use this mechanism, at least yet. It
|
||||||
// has a different last-user-wins auth model.
|
// has a different last-user-wins auth model.
|
||||||
|
@ -342,6 +342,10 @@ func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
||||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||||
return rw
|
return rw
|
||||||
}
|
}
|
||||||
|
if operatorUID != "" && uid == operatorUID {
|
||||||
|
logf("connection from userid %v; is configured operator", uid)
|
||||||
|
return rw
|
||||||
|
}
|
||||||
var adminGroupID string
|
var adminGroupID string
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
|
@ -435,7 +439,7 @@ func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
if ci.IsUnixSock {
|
if ci.IsUnixSock {
|
||||||
return true, !isReadonlyConn(ci, logger.Discard)
|
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
|
||||||
}
|
}
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,10 @@ type Prefs struct {
|
||||||
// Tailscale, if at all.
|
// Tailscale, if at all.
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
|
||||||
|
// OperatorUser is the local machine user name who is allowed to
|
||||||
|
// operate tailscaled without being root or using sudo.
|
||||||
|
OperatorUser string `json:",omitempty"`
|
||||||
|
|
||||||
// The Persist field is named 'Config' in the file for backward
|
// The Persist field is named 'Config' in the file for backward
|
||||||
// compatibility with earlier versions.
|
// compatibility with earlier versions.
|
||||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||||
|
@ -183,6 +187,7 @@ type MaskedPrefs struct {
|
||||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||||
NoSNATSet bool `json:",omitempty"`
|
NoSNATSet bool `json:",omitempty"`
|
||||||
NetfilterModeSet bool `json:",omitempty"`
|
NetfilterModeSet bool `json:",omitempty"`
|
||||||
|
OperatorUserSet bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
|
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
|
||||||
|
@ -273,6 +278,9 @@ func (p *Prefs) pretty(goos string) string {
|
||||||
if p.Hostname != "" {
|
if p.Hostname != "" {
|
||||||
fmt.Fprintf(&sb, "host=%q ", p.Hostname)
|
fmt.Fprintf(&sb, "host=%q ", p.Hostname)
|
||||||
}
|
}
|
||||||
|
if p.OperatorUser != "" {
|
||||||
|
fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
|
||||||
|
}
|
||||||
if p.Persist != nil {
|
if p.Persist != nil {
|
||||||
sb.WriteString(p.Persist.Pretty())
|
sb.WriteString(p.Persist.Pretty())
|
||||||
} else {
|
} else {
|
||||||
|
@ -311,6 +319,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||||
p.ShieldsUp == p2.ShieldsUp &&
|
p.ShieldsUp == p2.ShieldsUp &&
|
||||||
p.NoSNAT == p2.NoSNAT &&
|
p.NoSNAT == p2.NoSNAT &&
|
||||||
p.NetfilterMode == p2.NetfilterMode &&
|
p.NetfilterMode == p2.NetfilterMode &&
|
||||||
|
p.OperatorUser == p2.OperatorUser &&
|
||||||
p.Hostname == p2.Hostname &&
|
p.Hostname == p2.Hostname &&
|
||||||
p.OSVersion == p2.OSVersion &&
|
p.OSVersion == p2.OSVersion &&
|
||||||
p.DeviceModel == p2.DeviceModel &&
|
p.DeviceModel == p2.DeviceModel &&
|
||||||
|
|
|
@ -51,5 +51,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||||
AdvertiseRoutes []netaddr.IPPrefix
|
AdvertiseRoutes []netaddr.IPPrefix
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
OperatorUser string
|
||||||
Persist *persist.Persist
|
Persist *persist.Persist
|
||||||
}{})
|
}{})
|
||||||
|
|
|
@ -33,7 +33,28 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||||
func TestPrefsEqual(t *testing.T) {
|
func TestPrefsEqual(t *testing.T) {
|
||||||
tstest.PanicOnLog()
|
tstest.PanicOnLog()
|
||||||
|
|
||||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "ExitNodeAllowLANAccess", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
prefsHandles := []string{
|
||||||
|
"ControlURL",
|
||||||
|
"RouteAll",
|
||||||
|
"AllowSingleHosts",
|
||||||
|
"ExitNodeID",
|
||||||
|
"ExitNodeIP",
|
||||||
|
"ExitNodeAllowLANAccess",
|
||||||
|
"CorpDNS",
|
||||||
|
"WantRunning",
|
||||||
|
"ShieldsUp",
|
||||||
|
"AdvertiseTags",
|
||||||
|
"Hostname",
|
||||||
|
"OSVersion",
|
||||||
|
"DeviceModel",
|
||||||
|
"NotepadURLs",
|
||||||
|
"ForceDaemon",
|
||||||
|
"AdvertiseRoutes",
|
||||||
|
"NoSNAT",
|
||||||
|
"NetfilterMode",
|
||||||
|
"OperatorUser",
|
||||||
|
"Persist",
|
||||||
|
}
|
||||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||||
have, prefsHandles)
|
have, prefsHandles)
|
||||||
|
|
Loading…
Reference in New Issue