From f95e32e08975642a4f09489fecf29f47cd1360c8 Mon Sep 17 00:00:00 2001 From: Edoardo Spadolini Date: Wed, 6 Aug 2025 15:32:14 +0200 Subject: [PATCH] Allow ignoring errors in tctl workload-identity x509-issuer-overrides sign-csrs --- .../workload-identity/issuer-override.mdx | 2 + tool/tctl/common/workload_identity_command.go | 55 ++++++++++++---- ...orkload_identity_command_overrides_test.go | 64 ++++++++++++++++++- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/docs/pages/reference/workload-identity/issuer-override.mdx b/docs/pages/reference/workload-identity/issuer-override.mdx index 1e850105296f3..0f500ba1234eb 100644 --- a/docs/pages/reference/workload-identity/issuer-override.mdx +++ b/docs/pages/reference/workload-identity/issuer-override.mdx @@ -58,6 +58,8 @@ SERIALNUMBER=234567890123456789012345678901234567890,CN=clustername,O=clusternam Use of this command requires `create` permissions for the `workload_identity_x509_issuer_override_csr` resource kind in one of the roles associated with the identity running the command. +In clusters that make use of Hardware Security Modules (HSMs) it's possible that no single Teleport Auth Service instance is capable of generating signatures for all the keys that make up the SPIFFE certificate authority at once. In such situations, it's possible to use the `--force` option with the `sign-csrs` command on each machine running the Auth Service, to gather CSRs for keys managed by the different HSMs. + ## Using `tctl` to create a `workload_identity_x509_issuer_override` from certificate chain PEM files The `tctl workload-identity x509-issuer-overrides create` command can be used to build a `workload_identity_x509_issuer_override` resource out of one or more PEM files containing a certificate chain each, and to create or forcibly overwrite an existing resource in the cluster. The command will check that the first certificates in the specified chains have different public keys, and that they match 1:1 with the trusted X.509 certificates in the SPIFFE certificate authority in the Teleport cluster. diff --git a/tool/tctl/common/workload_identity_command.go b/tool/tctl/common/workload_identity_command.go index 8286dfe30b52e..33d8cdfdf475d 100644 --- a/tool/tctl/common/workload_identity_command.go +++ b/tool/tctl/common/workload_identity_command.go @@ -68,8 +68,9 @@ type WorkloadIdentityCommand struct { revocationReason string revocationExpiry string - overridesSignCmd *kingpin.CmdClause - overridesSignMode workloadidentityv1pb.CSRCreationMode + overridesSignCmd *kingpin.CmdClause + overridesSignMode workloadidentityv1pb.CSRCreationMode + overridesSignForce bool overridesCreateCmd *kingpin.CmdClause overridesCreateName string @@ -175,6 +176,10 @@ func (c *WorkloadIdentityCommand) Initialize( )). StringVar(&overridesSignMode) c.overridesSignMode = workloadidentityv1pb.CSRCreationMode_CSR_CREATION_MODE_SAME + c.overridesSignCmd. + Flag("force", "Attempt to sign as many CSRs as possible even in the presence of errors."). + Short('f'). + BoolVar(&c.overridesSignForce) c.overridesCreateCmd = overridesCmd.Command("create", "Create an issuer override from the given certificate chains.") c.overridesCreateCmd. @@ -654,31 +659,55 @@ func (c *WorkloadIdentityCommand) runOverridesSignCSRs(ctx context.Context, clie } keypairs := ca.GetTrustedTLSKeyPairs() - csrs := make([]*x509.CertificateRequest, 0, len(keypairs)) + type result struct { + issuer *x509.Certificate + csr *x509.CertificateRequest + err error + } + results := make([]result, 0, len(keypairs)) for _, kp := range keypairs { - block, _ := pem.Decode(kp.Cert) - if block == nil { - return trace.BadParameter("failed to decode PEM block in SPIFFE CA") + issuer, err := tlsca.ParseCertificatePEM(kp.Cert) + if err != nil { + return trace.Wrap(err) } resp, err := oclt.SignX509IssuerCSR(ctx, &workloadidentityv1pb.SignX509IssuerCSRRequest{ - Issuer: block.Bytes, + Issuer: issuer.Raw, CsrCreationMode: c.overridesSignMode, }) if err != nil { - return trace.Wrap(err) + if !c.overridesSignForce { + return trace.Wrap(err) + } + results = append(results, result{ + issuer: issuer, + csr: nil, + err: err, + }) + continue } csr, err := x509.ParseCertificateRequest(resp.GetCsr()) if err != nil { return trace.Wrap(err) } - csrs = append(csrs, csr) + results = append(results, result{ + issuer: issuer, + csr: csr, + err: nil, + }) } - for _, csr := range csrs { - fmt.Fprintln(c.stdout, csr.Subject) + + var errs []error + for _, r := range results { + fmt.Fprintln(c.stdout, r.issuer.Subject) + if r.err != nil { + errs = append(errs, r.err) + fmt.Fprintln(c.stdout, r.err.Error()) + continue + } _ = pem.Encode(c.stdout, &pem.Block{ Type: "CERTIFICATE REQUEST", - Bytes: csr.Raw, + Bytes: r.csr.Raw, }) } - return nil + return trace.Wrap(trace.NewAggregate(errs...), "some or all signature requests failed") } diff --git a/tool/tctl/common/workload_identity_command_overrides_test.go b/tool/tctl/common/workload_identity_command_overrides_test.go index b8df7b9541fa1..262b6e15261f6 100644 --- a/tool/tctl/common/workload_identity_command_overrides_test.go +++ b/tool/tctl/common/workload_identity_command_overrides_test.go @@ -215,7 +215,7 @@ func TestOverrideSign(t *testing.T) { l, err := out.ReadString('\n') require.NoError(t, err) - require.Equal(t, "CN=csr1\n", l) + require.Equal(t, "CN=c1\n", l) b, rest := pem.Decode(out.Bytes()) require.NotNil(t, b) require.Equal(t, "CERTIFICATE REQUEST", b.Type) @@ -224,7 +224,7 @@ func TestOverrideSign(t *testing.T) { l, err = out.ReadString('\n') require.NoError(t, err) - require.Equal(t, "CN=csr2\n", l) + require.Equal(t, "CN=c2\n", l) b, rest = pem.Decode(out.Bytes()) require.NotNil(t, b) require.Equal(t, "CERTIFICATE REQUEST", b.Type) @@ -232,6 +232,62 @@ func TestOverrideSign(t *testing.T) { out.Next(out.Len() - len(rest)) require.Zero(t, out.Len()) + + clt = runFakeAPIServer(t, func(s *grpc.Server) { + proto.RegisterAuthServiceServer(s, &domainNameServer{ + domainName: "clustername", + }) + trustv1.RegisterTrustServiceServer(s, &caServer{ + cas: map[types.CertAuthID]*types.CertAuthorityV2{ + {Type: types.SPIFFECA, DomainName: "clustername"}: { + Spec: types.CertAuthoritySpecV2{ + ActiveKeys: types.CAKeySet{ + TLS: []*types.TLSKeyPair{ + {Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c1.Leaf.Raw})}, + {Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c2.Leaf.Raw})}, + }, + }, + }, + }, + }, + }) + workloadidentityv1.RegisterX509OverridesServiceServer(s, &csrServer{ + resp: map[string][]byte{ + string(c1.Leaf.Raw): csr1, + }, + }) + }) + + out.Reset() + + err = runCommand(t, clt, &WorkloadIdentityCommand{stdout: out}, []string{ + "workload-identity", "x509-issuer-overrides", "sign-csrs", + }) + require.ErrorAs(t, err, new(*trace.NotFoundError)) + require.Zero(t, out.Len()) + + err = runCommand(t, clt, &WorkloadIdentityCommand{stdout: out}, []string{ + "workload-identity", "x509-issuer-overrides", "sign-csrs", "--force", + }) + require.ErrorAs(t, err, new(*trace.NotFoundError)) + + l, err = out.ReadString('\n') + require.NoError(t, err) + require.Equal(t, "CN=c1\n", l) + b, rest = pem.Decode(out.Bytes()) + require.NotNil(t, b) + require.Equal(t, "CERTIFICATE REQUEST", b.Type) + require.Equal(t, csr1, b.Bytes) + out.Next(out.Len() - len(rest)) + + l, err = out.ReadString('\n') + require.NoError(t, err) + require.Equal(t, "CN=c2\n", l) + l, err = out.ReadString('\n') + require.NoError(t, err) + require.Equal(t, signKeyNotFoundMessage+"\n", l) + + require.Zero(t, out.Len()) } type domainNameServer struct { @@ -292,13 +348,15 @@ type csrServer struct { resp map[string][]byte } +const signKeyNotFoundMessage = "key not found or something idk" + func (s *csrServer) SignX509IssuerCSR(ctx context.Context, req *workloadidentityv1.SignX509IssuerCSRRequest) (*workloadidentityv1.SignX509IssuerCSRResponse, error) { if req.GetCsrCreationMode() == 0 { return nil, status.Errorf(codes.InvalidArgument, "") } csr, ok := s.resp[string(req.GetIssuer())] if !ok { - return nil, status.Errorf(codes.NotFound, "") + return nil, status.Errorf(codes.NotFound, signKeyNotFoundMessage) } return &workloadidentityv1.SignX509IssuerCSRResponse{Csr: csr}, nil }