diff --git a/lib/auth/machineid/workloadidentityv1/decision.go b/lib/auth/machineid/workloadidentityv1/decision.go index 954324e253b7a..47545ff1b4c5d 100644 --- a/lib/auth/machineid/workloadidentityv1/decision.go +++ b/lib/auth/machineid/workloadidentityv1/decision.go @@ -27,10 +27,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 { @@ -40,6 +40,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, @@ -79,9 +90,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 1d581864c7d9e..a532eb3590063 100644 --- a/lib/auth/machineid/workloadidentityv1/decision_test.go +++ b/lib/auth/machineid/workloadidentityv1/decision_test.go @@ -69,7 +69,7 @@ func Test_decide(t *testing.T) { attrs: standardAttrs, wantIssue: false, 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 name") + 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 ea4da0a6d512e..b5bed74ed9034 100644 --- a/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go +++ b/lib/auth/machineid/workloadidentityv1/workloadidentityv1_test.go @@ -505,7 +505,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", }, }, @@ -849,7 +849,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 @@ -902,7 +902,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(),