diff --git a/api/gen/proto/go/teleport/scopes/joining/v1/token.pb.go b/api/gen/proto/go/teleport/scopes/joining/v1/token.pb.go index da10179de43..65dab850630 100644 --- a/api/gen/proto/go/teleport/scopes/joining/v1/token.pb.go +++ b/api/gen/proto/go/teleport/scopes/joining/v1/token.pb.go @@ -750,8 +750,9 @@ type AWS struct { // allow rule in order to use this token. Allow []*AWS_Rule `protobuf:"bytes,1,rep,name=allow,proto3" json:"allow,omitempty"` // The TTL to use for AWS EC2 Instance Identity Documents used - // to join the cluster with this token. - AwsIidTtl int64 `protobuf:"varint,2,opt,name=aws_iid_ttl,json=awsIidTtl,proto3" json:"aws_iid_ttl,omitempty"` + // to join the cluster with this token. This should be a duration + // string such as "8h" or "6mo". + IidTtl string `protobuf:"bytes,2,opt,name=iid_ttl,json=iidTtl,proto3" json:"iid_ttl,omitempty"` // Integration name which provides credentials for validating join attempts. // Currently only in use for validating the AWS Organization ID in the IAM Join method. Integration string `protobuf:"bytes,3,opt,name=integration,proto3" json:"integration,omitempty"` @@ -796,11 +797,11 @@ func (x *AWS) GetAllow() []*AWS_Rule { return nil } -func (x *AWS) GetAwsIidTtl() int64 { +func (x *AWS) GetIidTtl() string { if x != nil { - return x.AwsIidTtl + return x.IidTtl } - return 0 + return "" } func (x *AWS) GetIntegration() string { @@ -1478,10 +1479,10 @@ const file_teleport_scopes_joining_v1_token_proto_rawDesc = "" + "\x05scope\x18\x05 \x01(\tR\x05scope\x12F\n" + "\x04spec\x18\x06 \x01(\v22.teleport.scopes.joining.v1.StaticScopedTokensSpecR\x04spec\"Y\n" + "\x16StaticScopedTokensSpec\x12?\n" + - "\x06tokens\x18\x01 \x03(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x06tokens\"\xb2\x02\n" + + "\x06tokens\x18\x01 \x03(\v2'.teleport.scopes.joining.v1.ScopedTokenR\x06tokens\"\xab\x02\n" + "\x03AWS\x12:\n" + - "\x05allow\x18\x01 \x03(\v2$.teleport.scopes.joining.v1.AWS.RuleR\x05allow\x12\x1e\n" + - "\vaws_iid_ttl\x18\x02 \x01(\x03R\tawsIidTtl\x12 \n" + + "\x05allow\x18\x01 \x03(\v2$.teleport.scopes.joining.v1.AWS.RuleR\x05allow\x12\x17\n" + + "\aiid_ttl\x18\x02 \x01(\tR\x06iidTtl\x12 \n" + "\vintegration\x18\x03 \x01(\tR\vintegration\x1a\xac\x01\n" + "\x04Rule\x12\x1f\n" + "\vaws_account\x18\x01 \x01(\tR\n" + diff --git a/api/proto/teleport/scopes/joining/v1/token.proto b/api/proto/teleport/scopes/joining/v1/token.proto index 0bccbc974da..7d3c31a6bf8 100644 --- a/api/proto/teleport/scopes/joining/v1/token.proto +++ b/api/proto/teleport/scopes/joining/v1/token.proto @@ -196,8 +196,9 @@ message AWS { repeated Rule allow = 1; // The TTL to use for AWS EC2 Instance Identity Documents used - // to join the cluster with this token. - int64 aws_iid_ttl = 2; + // to join the cluster with this token. This should be a duration + // string such as "8h" or "6mo". + string iid_ttl = 2; // Integration name which provides credentials for validating join attempts. // Currently only in use for validating the AWS Organization ID in the IAM Join method. diff --git a/api/types/duration.go b/api/types/duration.go index bd146599d48..8e70e3fc687 100644 --- a/api/types/duration.go +++ b/api/types/duration.go @@ -57,7 +57,7 @@ func (d *Duration) UnmarshalJSON(data []byte) error { *d = Duration(0) return nil } - out, err := parseDuration(stringVar) + out, err := ParseDuration(stringVar) if err != nil { return trace.BadParameter("%s", err) } @@ -81,7 +81,7 @@ func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { *d = Duration(0) return nil } - out, err := parseDuration(stringVar) + out, err := ParseDuration(stringVar) if err != nil { return trace.BadParameter("%s", err) } @@ -165,12 +165,12 @@ var unitMap = map[string]int64{ "y": int64(time.Hour * 24 * 365), } -// parseDuration parses a duration string. +// ParseDuration parses a duration string. // A duration string is a possibly signed sequence of // decimal numbers, each with optional fraction and a unit suffix, // such as "300ms", "-1.5h" or "2h45m". // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -func parseDuration(s string) (Duration, error) { +func ParseDuration(s string) (Duration, error) { // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ orig := s var d int64 diff --git a/api/types/fuzz_test.go b/api/types/fuzz_test.go index c6a81b668cc..eb82eba5258 100644 --- a/api/types/fuzz_test.go +++ b/api/types/fuzz_test.go @@ -30,7 +30,7 @@ func FuzzParseDuration(f *testing.F) { f.Fuzz(func(t *testing.T, s string) { require.NotPanics(t, func() { - parseDuration(s) + ParseDuration(s) }) }) } diff --git a/buf.yaml b/buf.yaml index 448dfe6c7e3..3f464efad57 100644 --- a/buf.yaml +++ b/buf.yaml @@ -94,6 +94,8 @@ breaking: - api/proto/teleport/decision/v1alpha1 # TODO(nklaassen): Remove ignore once the new join API is stable. - api/proto/teleport/join/v1 + # TODO(eriktate): Remove ignore once the new scopes API is stable. + - api/proto/teleport/scopes plugins: - plugin: diff --git a/lib/join/jointest/scoped_token.go b/lib/join/jointest/scoped_token.go index 789185dec4d..ce72cd37540 100644 --- a/lib/join/jointest/scoped_token.go +++ b/lib/join/jointest/scoped_token.go @@ -69,8 +69,8 @@ func ScopedTokenFromProvisionTokenSpec(base types.ProvisionTokenSpecV2, override } } scopedToken.Spec.Aws = &joiningv1.AWS{ - Allow: allow, - AwsIidTtl: int64(base.AWSIIDTTL), + Allow: allow, + IidTtl: base.AWSIIDTTL.Duration().String(), } case types.JoinMethodIAM: allow := make([]*joiningv1.AWS_Rule, len(base.Allow)) diff --git a/lib/scopes/joining/token.go b/lib/scopes/joining/token.go index a830b0bf5a4..6b896c1ed35 100644 --- a/lib/scopes/joining/token.go +++ b/lib/scopes/joining/token.go @@ -53,7 +53,13 @@ func validateJoinMethod(token *joiningv1.ScopedToken) error { if token.GetStatus().GetSecret() == "" { return trace.BadParameter("secret value must be defined for a scoped token when using the token join method") } - case types.JoinMethodEC2, types.JoinMethodIAM: + case types.JoinMethodEC2: + ttl := token.GetSpec().GetAws().GetIidTtl() + if _, err := types.ParseDuration(ttl); ttl != "" && err != nil { + return trace.BadParameter("invalid IID TTL value %q, must be empty or a valid duration string (e.g. 30m, 12h, 1mo)", ttl) + } + fallthrough + case types.JoinMethodIAM: if len(token.GetSpec().GetAws().GetAllow()) == 0 { return trace.BadParameter("aws configuration must be defined for a scoped token when using the ec2 or iam join methods") } @@ -345,12 +351,13 @@ func (t *Token) GetAWSAllowRules() []*types.TokenRule { // GetAWSIIDTTL returns the TTL of EC2 IIDs func (t *Token) GetAWSIIDTTL() types.Duration { - ttl := t.scoped.GetSpec().GetAws().GetAwsIidTtl() - if ttl == 0 { - // default to 5 minute ttl if unspecified + ttl, err := types.ParseDuration(t.scoped.GetSpec().GetAws().GetIidTtl()) + if err != nil { + // if parsing fails for any reason (including an empty value) we fallback to the 5 minute default return types.Duration(5 * time.Minute) } - return types.Duration(ttl) + + return ttl } // GetIntegration returns the Integration field which is used to provide diff --git a/lib/scopes/joining/token_test.go b/lib/scopes/joining/token_test.go index a8f2b8e09cc..60a39f66ffd 100644 --- a/lib/scopes/joining/token_test.go +++ b/lib/scopes/joining/token_test.go @@ -233,6 +233,22 @@ func TestValidateScopedToken(t *testing.T) { expectedStrongErr: "aws configuration must be defined for a scoped token when using the ec2 or iam join methods", expectedWeakErr: "aws configuration must be defined for a scoped token when using the ec2 or iam join methods", }, + { + name: "ec2 token with invalid IID TTL", + modFn: func(tok *joiningv1.ScopedToken) { + tok.Spec.JoinMethod = string(types.JoinMethodEC2) + tok.Spec.Aws = &joiningv1.AWS{ + Allow: []*joiningv1.AWS_Rule{ + { + AwsAccount: "1234567890", + }, + }, + IidTtl: "123", // no unit specified + } + }, + expectedStrongErr: "invalid IID TTL value", + expectedWeakErr: "invalid IID TTL value", + }, { name: "iam token without aws configuration", modFn: func(tok *joiningv1.ScopedToken) { @@ -277,7 +293,21 @@ func TestValidateScopedToken(t *testing.T) { name: "valid scoped token", }, { - name: "valid ec2 scoped token", + name: "valid ec2 scoped token with TTL", + modFn: func(tok *joiningv1.ScopedToken) { + tok.Spec.JoinMethod = string(types.JoinMethodEC2) + tok.Spec.Aws = &joiningv1.AWS{ + Allow: []*joiningv1.AWS_Rule{ + { + AwsAccount: "1234567890", + }, + }, + IidTtl: "6mo", + } + }, + }, + { + name: "valid ec2 scoped token without TTL", modFn: func(tok *joiningv1.ScopedToken) { tok.Spec.JoinMethod = string(types.JoinMethodEC2) tok.Spec.Aws = &joiningv1.AWS{