diff --git a/lib/auth/machineid/workloadidentityv1/decision.go b/lib/auth/machineid/workloadidentityv1/decision.go index 63812c7a13c3f..b922d42e4dd86 100644 --- a/lib/auth/machineid/workloadidentityv1/decision.go +++ b/lib/auth/machineid/workloadidentityv1/decision.go @@ -26,10 +26,10 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/structpb" + "k8s.io/apimachinery/pkg/util/validation" workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" "github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1/expression" - "github.com/gravitational/teleport/lib/utils" ) type decision struct { @@ -39,6 +39,17 @@ type decision struct { sigstorePolicyResults map[string]error } +// validDNSSAN returns if the string is a valid value for a DNS SAN. This +// permits not only valid DNS 1123 subdomains but also wildcards of those. +func validDNSSAN(str string) bool { + isValidDomain := len(validation.IsDNS1123Subdomain(str)) == 0 + isValidWildcard := len(validation.IsWildcardDNS1123Subdomain(str)) == 0 + if isValidDomain || isValidWildcard { + return true + } + return false +} + func decide( ctx context.Context, wi *workloadidentityv1pb.WorkloadIdentity, @@ -78,9 +89,9 @@ func decide( d.reason = trace.Wrap(err, "templating spec.spiffe.x509.dns_sans[%d]", i) return d } - if !utils.IsValidHostname(templated) { + if !validDNSSAN(templated) { d.reason = trace.BadParameter( - "templating spec.spiffe.x509.dns_sans[%d] resulted in an invalid DNS name %q", + "templating spec.spiffe.x509.dns_sans[%d] resulted in an invalid DNS SAN %q", i, templated, ) diff --git a/lib/auth/machineid/workloadidentityv1/decision_test.go b/lib/auth/machineid/workloadidentityv1/decision_test.go index 4c87199240fcd..b04868769d388 100644 --- a/lib/auth/machineid/workloadidentityv1/decision_test.go +++ b/lib/auth/machineid/workloadidentityv1/decision_test.go @@ -68,8 +68,8 @@ func Test_decide(t *testing.T) { }, attrs: standardAttrs, wantIssue: false, - assertReason: func(t require.TestingT, err error, i ...interface{}) { - require.ErrorContains(t, err, "templating spec.spiffe.x509.dns_sans[0] resulted in an invalid DNS name") + assertReason: func(t require.TestingT, err error, i ...any) { + require.ErrorContains(t, err, "templating spec.spiffe.x509.dns_sans[0] resulted in an invalid DNS SAN") }, }, } @@ -599,3 +599,44 @@ func TestTemplateExtraClaims_TooDeeplyNested(t *testing.T) { _, err = templateExtraClaims(rawClaims, &workloadidentityv1pb.Attrs{}) require.ErrorContains(t, err, "cannot contain more than 10 levels of nesting") } + +func Test_validDNSSAN(t *testing.T) { + tests := []struct { + str string + want bool + }{ + { + str: "example.com", + want: true, + }, + { + // Single-label domain name. An unusual case but important since we + // may be issuing certs for hostnames in a local network (which will + // not abide by usual public internet dns semantics). + str: "example", + want: true, + }, + { + // DNS 1123 permits numeric characters at start (unlike RFC 1035) + str: "123-example.com", + want: true, + }, + { + str: "*.example.com", + want: true, + }, + { + str: "*example.com", + want: false, + }, + { + str: ".example.com", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.str, func(t *testing.T) { + require.Equal(t, tt.want, validDNSSAN(tt.str)) + }) + } +} diff --git a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go index 482e669bdd6b8..fe9558c273234 100644 --- a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go +++ b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go @@ -496,7 +496,7 @@ func TestIssueWorkloadIdentity(t *testing.T) { Hint: "Wow - what a lovely hint, {{user.name}}!", X509: &workloadidentityv1pb.WorkloadIdentitySPIFFEX509{ DnsSans: []string{ - "example.com", + "*.example.com", "{{user.name}}.example.com", }, }, @@ -838,7 +838,7 @@ func TestIssueWorkloadIdentity(t *testing.T) { require.WithinDuration(t, tp.clock.Now().Add(-1*time.Minute), cert.NotBefore, time.Second) // Check cert TTL require.Equal(t, cert.NotAfter.Sub(cert.NotBefore), wantTTL+time.Minute) - require.Equal(t, []string{"example.com", "dog.example.com"}, cert.DNSNames) + require.Equal(t, []string{"*.example.com", "dog.example.com"}, cert.DNSNames) // Check against SPIFFE SPEC // References are to https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md @@ -889,7 +889,7 @@ func TestIssueWorkloadIdentity(t *testing.T) { WorkloadIdentity: full.GetMetadata().GetName(), WorkloadIdentityRevision: full.GetMetadata().GetRevision(), DNSSANs: []string{ - "example.com", + "*.example.com", "dog.example.com", }, NameSelector: full.GetMetadata().GetName(),