Fix AWS IID TTL type in scoped tokens (#64026)

* changing the int64 field aws_iid_ttl into a string field named iid_ttl
to support duration strings and reduce repetitive naming (e.g.
aws.iid_ttl instead of aws.aws_iid_ttl)

* export ParseDuration so we can take advantage of the month and year units

* replaces aws_iid_ttl with iid_ttl when returning GetAWSIIDTTL() from a scoped token

* ignoring scopes protos from breaking change linter while the feature is still experimental
This commit is contained in:
Erik Tate
2026-02-20 20:08:09 +00:00
committed by GitHub
parent 3cc7f128f5
commit f1245236e7
8 changed files with 64 additions and 23 deletions
@@ -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" +
@@ -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.
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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)
})
})
}
+2
View File
@@ -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:
+2 -2
View File
@@ -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))
+12 -5
View File
@@ -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
+31 -1
View File
@@ -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{