cmd/containerboot: allow for automatic ID token generation

Allow for optionally specifying an audience for containerboot. This is
passed to tailscale up to allow for containerboot to use automatic ID
token generation for authentication.

Updates https://github.com/tailscale/corp/issues/34430

Signed-off-by: Mario Minardi <mario@tailscale.com>
pull/11974/merge
Mario Minardi 4 days ago committed by Mario Minardi
parent 02af7c963c
commit e9d82767e5

@ -20,8 +20,12 @@
// - TS_ID_TOKEN: the ID token from the identity provider for workload identity federation.
// Must be used together with TS_CLIENT_ID. If the value begins with "file:", it is
// treated as a path to a file containing the token.
// - Note: TS_AUTHKEY is mutually exclusive with TS_CLIENT_ID, TS_CLIENT_SECRET, and TS_ID_TOKEN.
// TS_CLIENT_SECRET and TS_ID_TOKEN cannot be used together.
// - TS_AUDIENCE: the audience to use when requesting an ID token from a well-known identity provider
// to exchange with the control server for workload identity federation. Must be used together
// with TS_CLIENT_ID.
// - Note: TS_AUTHKEY is mutually exclusive with TS_CLIENT_ID, TS_CLIENT_SECRET, TS_ID_TOKEN,
// and TS_AUDIENCE.
// TS_CLIENT_SECRET, TS_ID_TOKEN, and TS_AUDIENCE cannot be used together.
// - TS_HOSTNAME: the hostname to request for the node.
// - TS_ROUTES: subnet routes to advertise. Explicitly setting it to an empty
// value will cause containerboot to stop acting as a subnet router for any
@ -78,7 +82,7 @@
// directory that containers tailscaled config in file. The config file needs to be
// named cap-<current-tailscaled-cap>.hujson. If this is set, TS_HOSTNAME,
// TS_EXTRA_ARGS, TS_AUTHKEY, TS_CLIENT_ID, TS_CLIENT_SECRET, TS_ID_TOKEN,
// TS_ROUTES, TS_ACCEPT_DNS env vars must not be set. If this is set,
// TS_ROUTES, TS_ACCEPT_DNS, TS_AUDIENCE env vars must not be set. If this is set,
// containerboot only runs `tailscaled --config <path-to-this-configfile>`
// and not `tailscale up` or `tailscale set`.
// The config file contents are currently read once on container start.

@ -26,6 +26,7 @@ type settings struct {
ClientID string
ClientSecret string
IDToken string
Audience string
Hostname string
Routes *string
// ProxyTargetIP is the destination IP to which all incoming
@ -92,6 +93,7 @@ func configFromEnv() (*settings, error) {
ClientID: defaultEnv("TS_CLIENT_ID", ""),
ClientSecret: defaultEnv("TS_CLIENT_SECRET", ""),
IDToken: defaultEnv("TS_ID_TOKEN", ""),
Audience: defaultEnv("TS_AUDIENCE", ""),
Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnvStringPointer("TS_ROUTES"),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
@ -247,17 +249,46 @@ func (s *settings) validate() error {
if s.TailnetTargetFQDN != "" && s.TailnetTargetIP != "" {
return errors.New("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
}
if s.TailscaledConfigFilePath != "" && (s.AcceptDNS != nil || s.AuthKey != "" || s.Routes != nil || s.ExtraArgs != "" || s.Hostname != "" || s.ClientID != "" || s.ClientSecret != "" || s.IDToken != "") {
return errors.New("TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS, TS_CLIENT_ID, TS_CLIENT_SECRET, TS_ID_TOKEN.")
if s.TailscaledConfigFilePath != "" &&
(s.AcceptDNS != nil ||
s.AuthKey != "" ||
s.Routes != nil ||
s.ExtraArgs != "" ||
s.Hostname != "" ||
s.ClientID != "" ||
s.ClientSecret != "" ||
s.IDToken != "" ||
s.Audience != "") {
conflictingArgs := []string{
"TS_HOSTNAME",
"TS_EXTRA_ARGS",
"TS_AUTHKEY",
"TS_ROUTES",
"TS_ACCEPT_DNS",
"TS_CLIENT_ID",
"TS_CLIENT_SECRET",
"TS_ID_TOKEN",
"TS_AUDIENCE",
}
return fmt.Errorf("TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR cannot be set in combination with %s.", strings.Join(conflictingArgs, ", "))
}
if s.IDToken != "" && s.ClientID == "" {
return errors.New("TS_ID_TOKEN is set but TS_CLIENT_ID is not set")
}
if s.Audience != "" && s.ClientID == "" {
return errors.New("TS_AUDIENCE is set but TS_CLIENT_ID is not set")
}
if s.IDToken != "" && s.ClientSecret != "" {
return errors.New("TS_ID_TOKEN and TS_CLIENT_SECRET cannot both be set")
}
if s.AuthKey != "" && (s.ClientID != "" || s.ClientSecret != "" || s.IDToken != "") {
return errors.New("TS_AUTHKEY cannot be used with TS_CLIENT_ID, TS_CLIENT_SECRET, or TS_ID_TOKEN")
if s.IDToken != "" && s.Audience != "" {
return errors.New("TS_ID_TOKEN and TS_AUDIENCE cannot both be set")
}
if s.Audience != "" && s.ClientSecret != "" {
return errors.New("TS_AUDIENCE and TS_CLIENT_SECRET cannot both be set")
}
if s.AuthKey != "" && (s.ClientID != "" || s.ClientSecret != "" || s.IDToken != "" || s.Audience != "") {
return errors.New("TS_AUTHKEY cannot be used with TS_CLIENT_ID, TS_CLIENT_SECRET, TS_ID_TOKEN, or TS_AUDIENCE.")
}
if s.AllowProxyingClusterTrafficViaIngress && s.UserspaceMode {
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is not supported in userspace mode")

@ -117,6 +117,7 @@ func TestValidateAuthMethods(t *testing.T) {
clientID string
clientSecret string
idToken string
audience string
errContains string
}{
{
@ -144,11 +145,21 @@ func TestValidateAuthMethods(t *testing.T) {
clientID: "client-id",
idToken: "id-token",
},
{
name: "wif_client_id_and_audience",
clientID: "client-id",
audience: "audience",
},
{
name: "id_token_without_client_id",
idToken: "id-token",
errContains: "TS_ID_TOKEN is set but TS_CLIENT_ID is not set",
},
{
name: "audience_without_client_id",
audience: "audience",
errContains: "TS_AUDIENCE is set but TS_CLIENT_ID is not set",
},
{
name: "authkey_with_client_secret",
authKey: "tskey-auth-xxx",
@ -156,12 +167,19 @@ func TestValidateAuthMethods(t *testing.T) {
errContains: "TS_AUTHKEY cannot be used with",
},
{
name: "authkey_with_wif",
name: "authkey_with_id_token",
authKey: "tskey-auth-xxx",
clientID: "client-id",
idToken: "id-token",
errContains: "TS_AUTHKEY cannot be used with",
},
{
name: "authkey_with_audience",
authKey: "tskey-auth-xxx",
clientID: "client-id",
audience: "audience",
errContains: "TS_AUTHKEY cannot be used with",
},
{
name: "id_token_with_client_secret",
clientID: "client-id",
@ -169,6 +187,20 @@ func TestValidateAuthMethods(t *testing.T) {
idToken: "id-token",
errContains: "TS_ID_TOKEN and TS_CLIENT_SECRET cannot both be set",
},
{
name: "id_token_with_audience",
clientID: "client-id",
idToken: "id-token",
audience: "audience",
errContains: "TS_ID_TOKEN and TS_AUDIENCE cannot both be set",
},
{
name: "audience_with_client_secret",
clientID: "client-id",
clientSecret: "tskey-client-xxx",
audience: "audience",
errContains: "TS_AUDIENCE and TS_CLIENT_SECRET cannot both be set",
},
}
for _, tt := range tests {
@ -178,6 +210,7 @@ func TestValidateAuthMethods(t *testing.T) {
ClientID: tt.clientID,
ClientSecret: tt.clientSecret,
IDToken: tt.idToken,
Audience: tt.audience,
}
err := s.validate()
if tt.errContains != "" {

@ -129,6 +129,9 @@ func tailscaleUp(ctx context.Context, cfg *settings) error {
if cfg.IDToken != "" {
args = append(args, "--id-token="+cfg.IDToken)
}
if cfg.Audience != "" {
args = append(args, "--audience="+cfg.Audience)
}
// --advertise-routes can be passed an empty string to configure a
// device (that might have previously advertised subnet routes) to not
// advertise any routes. Respect an empty string passed by a user and

Loading…
Cancel
Save