From aa31215a210736134e2ea765d373c66eeafd371f Mon Sep 17 00:00:00 2001 From: ehila Date: Thu, 17 Sep 2026 10:50:18 -0400 Subject: [PATCH] OCPSTRAT-2899: Add certificate status command Add read-only certificate inventory loading with service ownership and rotation policy metadata. Report deterministic human-readable and JSON certificate status using the enhancement's zone thresholds. Co-Authored-By: GPT-5 Signed-off-by: ehila --- cmd/microshift/main.go | 1 + pkg/cmd/certs.go | 261 ++++++++++++++++++ pkg/cmd/certs_test.go | 132 +++++++++ pkg/cmd/init.go | 159 +++++++---- .../certchains/chainsbuilder.go | 1 + .../cryptomaterial/certchains/inventory.go | 225 ++++++++++++++- .../certchains/inventory_test.go | 140 ++++++++++ .../certchains/signerbuilder.go | 11 + pkg/util/cryptomaterial/certchains/signers.go | 9 +- 9 files changed, 868 insertions(+), 71 deletions(-) create mode 100644 pkg/cmd/certs.go create mode 100644 pkg/cmd/certs_test.go diff --git a/cmd/microshift/main.go b/cmd/microshift/main.go index a35978c32b..4e0dac02d5 100644 --- a/cmd/microshift/main.go +++ b/cmd/microshift/main.go @@ -41,6 +41,7 @@ func newCommand() *cobra.Command { cmd.AddCommand(cmds.NewBackupCommand()) cmd.AddCommand(cmds.NewRestoreCommand()) cmd.AddCommand(cmds.NewHealthcheckCommand()) + cmd.AddCommand(cmds.NewCertsCommand(ioStreams)) cmd.AddCommand(cmds.NewAddNodeCommand()) cmd.AddCommand(cmds.NewC2CCProbeCommand()) return cmd diff --git a/pkg/cmd/certs.go b/pkg/cmd/certs.go new file mode 100644 index 0000000000..b0083061f1 --- /dev/null +++ b/pkg/cmd/certs.go @@ -0,0 +1,261 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "math" + "sort" + "strings" + "text/tabwriter" + "time" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/util/cryptomaterial" + "github.com/openshift/microshift/pkg/util/cryptomaterial/certchains" +) + +const ( + certificateOutputJSON = "json" + certificateStatusAPIVersion = "microshift.openshift.io/v1alpha1" +) + +type certificateStatusConfig struct { + ForceRestartOnRedZone bool `json:"forceRestartOnRedZone"` + ServingValidity string `json:"servingValidity"` + CAValidity string `json:"caValidity"` +} + +type certificateStatusItem struct { + Service string `json:"service"` + Name string `json:"name"` + Role certchains.CertificateRole `json:"role"` + RotationPolicy certchains.RotationPolicy `json:"rotationPolicy"` + Zone certchains.CertificateZone `json:"zone"` + NotBefore string `json:"notBefore"` + NotAfter string `json:"notAfter"` + RemainingSeconds int64 `json:"remainingSeconds"` + certificate certchains.CertificateInventoryEntry +} + +type certificateStatusList struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + GeneratedAt string `json:"generatedAt"` + Config certificateStatusConfig `json:"config"` + Items []certificateStatusItem `json:"items"` + Warnings []string `json:"warnings"` + generatedAt time.Time +} + +type certStatusOptions struct { + genericclioptions.IOStreams + + output string + now func() time.Time + loadConfig func() (*config.Config, error) + loadInventory func(*config.Config) (certchains.CertificateInventory, error) +} + +// NewCertsCommand creates the certificate administration command family. +func NewCertsCommand(ioStreams genericclioptions.IOStreams) *cobra.Command { + return newCertsCommand(ioStreams, shouldRunPrivileged) +} + +func newCertsCommand(ioStreams genericclioptions.IOStreams, requirePrivileges func() error) *cobra.Command { + command := &cobra.Command{ + Use: "certs", + Short: "Inspect and manage MicroShift certificates", + Args: cobra.NoArgs, + PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + return requirePrivileges() + }, + } + + options := &certStatusOptions{ + IOStreams: ioStreams, + now: time.Now, + loadConfig: config.ActiveConfig, + loadInventory: loadCertificateInventory, + } + command.AddCommand(newCertsStatusCommand(options)) + return command +} + +func newCertsStatusCommand(options *certStatusOptions) *cobra.Command { + command := &cobra.Command{ + Use: "status", + Short: "Report the status of managed MicroShift certificates", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return options.run() + }, + } + command.Flags().StringVarP(&options.output, "output", "o", "", "Output format. One of: json.") + return command +} + +func (o *certStatusOptions) run() error { + if o.output != "" && o.output != certificateOutputJSON { + return fmt.Errorf("unsupported output format %q; supported formats: json", o.output) + } + + cfg, err := o.loadConfig() + if err != nil { + return fmt.Errorf("failed to load MicroShift configuration: %w", err) + } + inventory, err := o.loadInventory(cfg) + if err != nil { + return fmt.Errorf("failed to load certificate inventory: %w", err) + } + + status, err := newCertificateStatusList(inventory, cfg.Warnings, o.now()) + if err != nil { + return fmt.Errorf("failed to build certificate status: %w", err) + } + if o.output == certificateOutputJSON { + return writeCertificateStatusJSON(o.Out, status) + } + for _, warning := range status.Warnings { + if _, err := fmt.Fprintf(o.ErrOut, "WARNING: %s\n", warning); err != nil { + return err + } + } + return writeCertificateStatusTable(o.Out, status) +} + +func loadCertificateInventory(cfg *config.Config) (certchains.CertificateInventory, error) { + builder, err := certificateChainsSetup(cfg) + if err != nil { + return nil, err + } + return builder.LoadInventory() +} + +func newCertificateStatusList(inventory certchains.CertificateInventory, warnings []string, now time.Time) (certificateStatusList, error) { + now = now.UTC().Truncate(time.Second) + items := make([]certificateStatusItem, 0, len(inventory)) + for _, entry := range inventory { + if entry.Name == "" { + return certificateStatusList{}, fmt.Errorf("certificate inventory entry has no name") + } + if entry.Service == "" { + return certificateStatusList{}, fmt.Errorf("certificate %q has no owning service", entry.Name) + } + switch entry.Role { + case certchains.CertificateRoleCA, certchains.CertificateRoleServing, certchains.CertificateRoleClient, certchains.CertificateRolePeer: + case certchains.CertificateRoleUnknown: + return certificateStatusList{}, fmt.Errorf("certificate %q has an unknown role", entry.Name) + default: + return certificateStatusList{}, fmt.Errorf("certificate %q has invalid role %q", entry.Name, entry.Role) + } + zone, err := entry.ZoneAt(now) + if err != nil { + return certificateStatusList{}, err + } + items = append(items, certificateStatusItem{ + Service: entry.Service, + Name: entry.Name, + Role: entry.Role, + RotationPolicy: entry.RotationPolicy, + Zone: zone, + NotBefore: entry.Certificate.NotBefore.UTC().Format(time.RFC3339), + NotAfter: entry.Certificate.NotAfter.UTC().Format(time.RFC3339), + RemainingSeconds: int64(entry.Certificate.NotAfter.Sub(now).Seconds()), + certificate: entry, + }) + } + + sort.Slice(items, func(i, j int) bool { + if items[i].Service != items[j].Service { + return items[i].Service < items[j].Service + } + if items[i].Name != items[j].Name { + return items[i].Name < items[j].Name + } + return strings.Join(items[i].certificate.Path, "/") < strings.Join(items[j].certificate.Path, "/") + }) + + statusWarnings := append([]string(nil), warnings...) + if statusWarnings == nil { + statusWarnings = []string{} + } + return certificateStatusList{ + APIVersion: certificateStatusAPIVersion, + Kind: "CertificateStatusList", + GeneratedAt: now.Format(time.RFC3339), + Config: certificateStatusConfig{ + ForceRestartOnRedZone: true, + ServingValidity: durationInHours(cryptomaterial.ShortLivedCertificateValidity), + CAValidity: durationInHours(cryptomaterial.LongLivedCertificateValidity), + }, + Items: items, + Warnings: statusWarnings, + generatedAt: now, + }, nil +} + +func writeCertificateStatusJSON(out io.Writer, status certificateStatusList) error { + encoder := json.NewEncoder(out) + encoder.SetIndent("", " ") + return encoder.Encode(status) +} + +func writeCertificateStatusTable(out io.Writer, status certificateStatusList) error { + w := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) + if _, err := fmt.Fprintln(w, "SERVICE\tCERTIFICATE\tSTATUS\tEXPIRY\tREASON\tMESSAGE"); err != nil { + return err + } + for _, item := range status.Items { + reason, message := humanCertificateStatus(item.certificate, item.Zone, status.generatedAt) + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + item.Service, + item.Name, + humanCertificateZone(item.Zone), + item.NotAfter, + reason, + message, + ); err != nil { + return err + } + } + return w.Flush() +} + +func humanCertificateStatus(certificate certchains.CertificateInventoryEntry, zone certchains.CertificateZone, now time.Time) (string, string) { + if now.Before(certificate.Certificate.NotBefore) { + return "NotYetValid", fmt.Sprintf("Valid in %d days", daysUntil(certificate.Certificate.NotBefore.Sub(now))) + } + if !now.Before(certificate.Certificate.NotAfter) { + return "Expired", fmt.Sprintf("Expired %d days ago", daysUntil(now.Sub(certificate.Certificate.NotAfter))) + } + remainingDays := daysUntil(certificate.Certificate.NotAfter.Sub(now)) + if zone == certchains.CertificateZoneGreen { + return "NotExpiring", fmt.Sprintf("Valid for %d days", remainingDays) + } + return "Expiring", fmt.Sprintf("Expires in %d days", remainingDays) +} + +func humanCertificateZone(zone certchains.CertificateZone) string { + switch zone { + case certchains.CertificateZoneGreen: + return "Green" + case certchains.CertificateZoneYellow: + return "Yellow" + case certchains.CertificateZoneRed: + return "Red" + default: + return string(zone) + } +} + +func daysUntil(duration time.Duration) int64 { + return int64(math.Ceil(duration.Hours() / 24)) +} + +func durationInHours(duration time.Duration) string { + return fmt.Sprintf("%dh", int64(duration.Hours())) +} diff --git a/pkg/cmd/certs_test.go b/pkg/cmd/certs_test.go new file mode 100644 index 0000000000..fe15f83a62 --- /dev/null +++ b/pkg/cmd/certs_test.go @@ -0,0 +1,132 @@ +package cmd + +import ( + "bytes" + "crypto/x509" + "errors" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/openshift/microshift/pkg/config" + "github.com/openshift/microshift/pkg/util/cryptomaterial/certchains" +) + +func TestNewCertificateStatusList(t *testing.T) { + now := time.Date(2026, time.September, 8, 10, 30, 0, 500, time.UTC) + inventory := certchains.CertificateInventory{ + certificateInventoryEntry("service-b", "cert-b", certchains.CertificateRoleCA, certchains.RotationPolicyExtended, now.Add(-900*time.Hour), now.Add(100*time.Hour)), + certificateInventoryEntry("service-a", "cert-z", certchains.CertificateRoleServing, certchains.RotationPolicyStandard, now.Add(-400*time.Hour), now.Add(600*time.Hour)), + certificateInventoryEntry("service-a", "cert-a", certchains.CertificateRoleClient, certchains.RotationPolicyStandard, now.Add(-500*time.Hour), now.Add(500*time.Hour)), + } + + status, err := newCertificateStatusList(inventory, nil, now) + require.NoError(t, err) + require.Equal(t, certificateStatusAPIVersion, status.APIVersion) + require.Equal(t, "CertificateStatusList", status.Kind) + require.Equal(t, "2026-09-08T10:30:00Z", status.GeneratedAt) + require.Equal(t, certificateStatusConfig{ + ForceRestartOnRedZone: true, + ServingValidity: "8760h", + CAValidity: "87600h", + }, status.Config) + require.Equal(t, []string{}, status.Warnings) + require.Equal(t, []string{"cert-a", "cert-z", "cert-b"}, []string{status.Items[0].Name, status.Items[1].Name, status.Items[2].Name}) + require.Equal(t, certchains.CertificateZoneYellow, status.Items[0].Zone) + require.Equal(t, int64((500*time.Hour)/time.Second), status.Items[0].RemainingSeconds) +} + +func TestCertStatusOutput(t *testing.T) { + now := time.Date(2026, time.September, 8, 10, 30, 0, 0, time.UTC) + status, err := newCertificateStatusList(certchains.CertificateInventory{ + certificateInventoryEntry("etcd", "etcd-serving", certchains.CertificateRolePeer, certchains.RotationPolicyExtended, now.Add(-900*time.Hour), now.Add(100*time.Hour)), + }, []string{"configuration warning"}, now) + require.NoError(t, err) + + var jsonOutput bytes.Buffer + require.NoError(t, writeCertificateStatusJSON(&jsonOutput, status)) + require.JSONEq(t, `{ + "apiVersion":"microshift.openshift.io/v1alpha1", + "kind":"CertificateStatusList", + "generatedAt":"2026-09-08T10:30:00Z", + "config":{"forceRestartOnRedZone":true,"servingValidity":"8760h","caValidity":"87600h"}, + "items":[{ + "service":"etcd", + "name":"etcd-serving", + "role":"peer", + "rotationPolicy":"extended", + "zone":"red", + "notBefore":"2026-08-01T22:30:00Z", + "notAfter":"2026-09-12T14:30:00Z", + "remainingSeconds":360000 + }], + "warnings":["configuration warning"] + }`, jsonOutput.String()) + + var tableOutput bytes.Buffer + require.NoError(t, writeCertificateStatusTable(&tableOutput, status)) + require.Contains(t, tableOutput.String(), "SERVICE CERTIFICATE STATUS EXPIRY") + require.Contains(t, tableOutput.String(), "etcd etcd-serving Red") + require.Contains(t, tableOutput.String(), "Expiring Expires in 5 days") +} + +func TestCertStatusOptionsRun(t *testing.T) { + now := time.Date(2026, time.September, 8, 10, 30, 0, 0, time.UTC) + var stdout bytes.Buffer + var stderr bytes.Buffer + warnings := []string(nil) + options := certStatusOptions{ + IOStreams: genericclioptions.IOStreams{Out: &stdout, ErrOut: &stderr}, + output: "json", + now: func() time.Time { return now }, + loadConfig: func() (*config.Config, error) { + return &config.Config{Warnings: warnings}, nil + }, + loadInventory: func(*config.Config) (certchains.CertificateInventory, error) { + return certchains.CertificateInventory{ + certificateInventoryEntry("etcd", "etcd-signer", certchains.CertificateRoleCA, certchains.RotationPolicyExtended, now.Add(-100*time.Hour), now.Add(900*time.Hour)), + }, nil + }, + } + require.NoError(t, options.run()) + require.Contains(t, stdout.String(), `"kind": "CertificateStatusList"`) + + stdout.Reset() + options.output = "" + warnings = []string{"configuration warning"} + require.NoError(t, options.run()) + require.NotContains(t, stdout.String(), "configuration warning") + require.Equal(t, "WARNING: configuration warning\n", stderr.String()) + + options.output = "yaml" + require.EqualError(t, options.run(), `unsupported output format "yaml"; supported formats: json`) +} + +func TestCertsCommandRequiresPrivileges(t *testing.T) { + wantErr := errors.New("privileges required") + command := newCertsCommand(genericclioptions.IOStreams{}, func() error { return wantErr }) + command.SetArgs([]string{"status"}) + require.ErrorIs(t, command.Execute(), wantErr) + + statusCommand, _, err := command.Find([]string{"status"}) + require.NoError(t, err) + require.NotNil(t, statusCommand.Flags().Lookup("output")) + require.Equal(t, "o", statusCommand.Flags().Lookup("output").Shorthand) +} + +func certificateInventoryEntry(service, name string, role certchains.CertificateRole, policy certchains.RotationPolicy, notBefore, notAfter time.Time) certchains.CertificateInventoryEntry { + return certchains.CertificateInventoryEntry{ + Path: strings.Split(name, "/"), + Name: name, + Service: service, + Role: role, + RotationPolicy: policy, + Certificate: x509.Certificate{ + NotBefore: notBefore, + NotAfter: notAfter, + }, + } +} diff --git a/pkg/cmd/init.go b/pkg/cmd/init.go index e692dd1cbe..7d6ad59aa1 100644 --- a/pkg/cmd/init.go +++ b/pkg/cmd/init.go @@ -56,6 +56,34 @@ func initCerts(cfg *config.Config) (*certchains.CertificateChains, error) { } func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { + builder, err := certificateChainsSetup(cfg) + if err != nil { + return nil, err + } + + certChains, err := builder.Complete() + if err != nil { + return nil, err + } + + saKeyDir := filepath.Join(config.DataDir, "/resources/kube-apiserver/secrets/service-account-key") + if err := util.EnsureKeyPair( + filepath.Join(saKeyDir, "service-account.pub"), + filepath.Join(saKeyDir, "service-account.key"), + ); err != nil { + return nil, err + } + + cfg.Ingress.ServingCertificate, cfg.Ingress.ServingKey, err = certChains.GetCertKey("ingress-ca", "router-default-serving") + if err != nil { + return nil, err + } + + return certChains, nil +} + +//nolint:ireturn // Callers choose read-only inventory loading or certificate creation. +func certificateChainsSetup(cfg *config.Config) (certchains.CertificateChainsBuilder, error) { // Anchor certificate expiration to the next day. This forces // homogenous expiry dates for all certificates with the same validity. startTime := time.Now() @@ -98,7 +126,7 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { certsDir := cryptomaterial.CertsDirectory(config.DataDir) - certChains, err := certchains.NewCertificateChains( + builder := certchains.NewCertificateChains( // ------------------------------ // CLIENT CERTIFICATE SIGNERS // ------------------------------ @@ -108,32 +136,40 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kube-control-plane-signer", cryptomaterial.KubeControlPlaneSignerCertDir(certsDir), alignValidity(cryptomaterial.ShortLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("control-plane").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "kube-controller-manager", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "kube-controller-manager", + Service: "kube-controller-manager", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:kube-controller-manager"}, }, &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "kube-scheduler", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "kube-scheduler", + Service: "kube-scheduler", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:kube-scheduler"}, }, &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "cluster-policy-controller", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "cluster-policy-controller", + Service: "cluster-policy-controller", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:kube-controller-manager"}, }, &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "route-controller-manager", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "route-controller-manager", + Service: "route-controller-manager", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: serviceaccount.UserInfo("openshift-route-controller-manager", "route-controller-manager-sa", ""), }), @@ -143,18 +179,22 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kube-apiserver-to-kubelet-signer", cryptomaterial.KubeAPIServerToKubeletSignerCertDir(certsDir), alignValidity(cryptomaterial.ShortLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("kube-apiserver").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "kube-apiserver-to-kubelet-client", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "kube-apiserver-to-kubelet-client", + Service: "kube-apiserver", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:kube-apiserver", Groups: []string{"kube-master"}}, }).WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "metrics-server-kubelet-client", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "metrics-server-kubelet-client", + Service: "metrics-server", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:metrics-server"}, }), @@ -164,18 +204,22 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "admin-kubeconfig-signer", cryptomaterial.AdminKubeconfigSignerDir(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("authentication").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "admin-kubeconfig-client", - Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + Name: "admin-kubeconfig-client", + Service: "authentication", + Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyExtended, }, UserInfo: &user.DefaultInfo{Name: "system:admin", Groups: []string{"system:masters"}}, }).WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "openshift-observability-client", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "openshift-observability-client", + Service: "observability", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "openshift-observability-client"}, }, @@ -186,16 +230,18 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kubelet-signer", cryptomaterial.KubeletCSRSignerSignerCertDir(certsDir), alignValidity(cryptomaterial.ShortLivedCertificateValidity), - ).WithSubCAs( + ).WithService("kubelet").WithSubCAs( certchains.NewCertificateSigner( "kube-csr-signer", cryptomaterial.CSRSignerCertDir(certsDir), alignValidity(cryptomaterial.ShortLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("kubelet").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "kubelet-client", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "kubelet-client", + Service: "kubelet", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, // userinfo per https://kubernetes.io/docs/reference/access-authn-authz/node/#overview UserInfo: &user.DefaultInfo{Name: "system:node:" + cfg.CanonicalNodeName(), Groups: []string{"system:nodes"}}, @@ -204,6 +250,7 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "kubelet-server", + Service: "kubelet", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: []string{cfg.Node.HostnameOverride, cfg.Node.NodeIP}, @@ -214,11 +261,13 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "aggregator-signer", cryptomaterial.AggregatorSignerDir(certsDir), alignValidity(cryptomaterial.ShortLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("kube-apiserver").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "aggregator-client", - Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + Name: "aggregator-client", + Service: "kube-apiserver", + Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyStandard, }, UserInfo: &user.DefaultInfo{Name: "system:openshift-aggregator"}, }, @@ -231,10 +280,11 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "service-ca", cryptomaterial.ServiceCADir(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithServingCertificates( + ).WithService("service-ca").WithServingCertificates( &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "route-controller-manager-serving", + Service: "route-controller-manager", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: []string{ @@ -248,10 +298,11 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "ingress-ca", cryptomaterial.IngressCADir(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithServingCertificates( + ).WithService("ingress").WithServingCertificates( &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "router-default-serving", + Service: "ingress", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: []string{ @@ -266,10 +317,11 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kube-apiserver-external-signer", cryptomaterial.KubeAPIServerExternalSigner(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithServingCertificates( + ).WithService("kube-apiserver").WithServingCertificates( &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "kube-external-serving", + Service: "kube-apiserver", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: externalCertNames, @@ -280,10 +332,11 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kube-apiserver-localhost-signer", cryptomaterial.KubeAPIServerLocalhostSigner(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithServingCertificates( + ).WithService("kube-apiserver").WithServingCertificates( &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "kube-apiserver-localhost-serving", + Service: "kube-apiserver", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: []string{ @@ -296,10 +349,11 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "kube-apiserver-service-network-signer", cryptomaterial.KubeAPIServerServiceNetworkSigner(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithServingCertificates( + ).WithService("kube-apiserver").WithServingCertificates( &certchains.ServingCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ Name: "kube-apiserver-service-network-serving", + Service: "kube-apiserver", Validity: alignValidity(cryptomaterial.ShortLivedCertificateValidity), }, Hostnames: []string{ @@ -326,27 +380,33 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { "etcd-signer", cryptomaterial.EtcdSignerDir(certsDir), alignValidity(cryptomaterial.LongLivedCertificateValidity), - ).WithClientCertificates( + ).WithService("etcd").WithClientCertificates( &certchains.ClientCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "apiserver-etcd-client", - Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + Name: "apiserver-etcd-client", + Service: "kube-apiserver", + Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyExtended, }, UserInfo: &user.DefaultInfo{Name: "etcd", Groups: []string{"etcd"}}, }, ).WithPeerCertificiates( &certchains.PeerCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "etcd-peer", - Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + Name: "etcd-peer", + Service: "etcd", + Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyExtended, }, UserInfo: &user.DefaultInfo{Name: "system:etcd-peer:etcd-client", Groups: []string{"system:etcd-peers"}}, Hostnames: []string{"localhost", cfg.Node.HostnameOverride, cfg.Node.NodeIP}, }, &certchains.PeerCertificateSigningRequestInfo{ CSRMeta: certchains.CSRMeta{ - Name: "etcd-serving", - Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + Name: "etcd-serving", + Service: "etcd", + Validity: alignValidity(cryptomaterial.LongLivedCertificateValidity), + RotationPolicy: certchains.RotationPolicyExtended, }, UserInfo: &user.DefaultInfo{Name: "system:etcd-server:etcd-client", Groups: []string{"system:etcd-servers"}}, Hostnames: []string{"localhost", cfg.Node.HostnameOverride, cfg.Node.NodeIP}, @@ -375,26 +435,9 @@ func certSetup(cfg *config.Config) (*certchains.CertificateChains, error) { []string{"kube-apiserver-localhost-signer"}, []string{"kube-apiserver-service-network-signer"}, []string{"kube-apiserver-external-signer"}, - ).Complete() - - if err != nil { - return nil, err - } - - saKeyDir := filepath.Join(config.DataDir, "/resources/kube-apiserver/secrets/service-account-key") - if err := util.EnsureKeyPair( - filepath.Join(saKeyDir, "service-account.pub"), - filepath.Join(saKeyDir, "service-account.key"), - ); err != nil { - return nil, err - } - - cfg.Ingress.ServingCertificate, cfg.Ingress.ServingKey, err = certChains.GetCertKey("ingress-ca", "router-default-serving") - if err != nil { - return nil, err - } + ) - return certChains, nil + return builder, nil } func initKubeconfigs( diff --git a/pkg/util/cryptomaterial/certchains/chainsbuilder.go b/pkg/util/cryptomaterial/certchains/chainsbuilder.go index 9b0b434ebd..915623d1ed 100644 --- a/pkg/util/cryptomaterial/certchains/chainsbuilder.go +++ b/pkg/util/cryptomaterial/certchains/chainsbuilder.go @@ -8,6 +8,7 @@ import ( type CertificateChainsBuilder interface { WithSigners(signers ...CertificateSignerBuilder) CertificateChainsBuilder WithCABundle(bundlePath string, signerNames ...[]string) CertificateChainsBuilder + LoadInventory() (CertificateInventory, error) Complete() (*CertificateChains, error) } diff --git a/pkg/util/cryptomaterial/certchains/inventory.go b/pkg/util/cryptomaterial/certchains/inventory.go index 722807f73d..765d8c72f1 100644 --- a/pkg/util/cryptomaterial/certchains/inventory.go +++ b/pkg/util/cryptomaterial/certchains/inventory.go @@ -1,6 +1,16 @@ package certchains -import "crypto/x509" +import ( + "crypto/x509" + "fmt" + "os" + "path/filepath" + "sort" + "time" + + "github.com/openshift/library-go/pkg/crypto" + "github.com/openshift/microshift/pkg/util/cryptomaterial" +) // CertificateRole identifies how a certificate is used by MicroShift. type CertificateRole string @@ -13,12 +23,34 @@ const ( CertificateRolePeer CertificateRole = "peer" ) +// RotationPolicy identifies the thresholds used to classify certificate zones. +type RotationPolicy string + +const ( + RotationPolicyUnknown RotationPolicy = "unknown" + RotationPolicyStandard RotationPolicy = "standard" + RotationPolicyExtended RotationPolicy = "extended" +) + +// CertificateZone identifies the current renewal urgency of a certificate. +type CertificateZone string + +const ( + CertificateZoneGreen CertificateZone = "green" + CertificateZoneYellow CertificateZone = "yellow" + CertificateZoneRed CertificateZone = "red" +) + // CertificateInventoryEntry describes one certificate managed by the chain. // Path can be passed to CertificateChains methods such as Regenerate. type CertificateInventoryEntry struct { - Path []string - Role CertificateRole - Certificate x509.Certificate + Path []string + Name string + Service string + ParentCA string + Role CertificateRole + RotationPolicy RotationPolicy + Certificate x509.Certificate } // CertificateInventory is a snapshot of certificates managed by a chain. @@ -35,6 +67,42 @@ func (i CertificateInventory) ByRole(role CertificateRole) CertificateInventory return entries } +// ZoneAt calculates the certificate's renewal zone at the provided time. +func (entry CertificateInventoryEntry) ZoneAt(now time.Time) (CertificateZone, error) { + certificate := entry.Certificate + if now.Before(certificate.NotBefore) || !now.Before(certificate.NotAfter) { + return CertificateZoneRed, nil + } + + validity := certificate.NotAfter.Sub(certificate.NotBefore) + if validity <= 0 { + return CertificateZoneRed, nil + } + remaining := certificate.NotAfter.Sub(now) + ratio := float64(remaining) / float64(validity) + + var greenThreshold, yellowThreshold float64 + switch entry.RotationPolicy { + case RotationPolicyStandard: + greenThreshold, yellowThreshold = 0.583, 0.333 + case RotationPolicyExtended: + greenThreshold, yellowThreshold = 0.15, 0.10 + case RotationPolicyUnknown: + return "", fmt.Errorf("certificate %q has unknown rotation policy %q", entry.Name, entry.RotationPolicy) + default: + return "", fmt.Errorf("certificate %q has unknown rotation policy %q", entry.Name, entry.RotationPolicy) + } + + switch { + case ratio > greenThreshold: + return CertificateZoneGreen, nil + case ratio > yellowThreshold: + return CertificateZoneYellow, nil + default: + return CertificateZoneRed, nil + } +} + // Inventory returns a deterministic snapshot of all certificates managed by // the chain. Signers are listed before their sub-CAs and leaf certificates. func (cs *CertificateChains) Inventory() CertificateInventory { @@ -50,9 +118,13 @@ func (cs *CertificateChains) Inventory() CertificateInventory { func (s *CertificateSigner) inventory(path []string) CertificateInventory { entries := make(CertificateInventory, 0, 1+len(s.subCAs)+len(s.signedCertificates)) entries = append(entries, CertificateInventoryEntry{ - Path: append([]string(nil), path...), - Role: CertificateRoleCA, - Certificate: *s.signerConfig.Config.Certs[0], + Path: append([]string(nil), path...), + Name: s.signerName, + Service: s.service, + ParentCA: parentCA(path), + Role: CertificateRoleCA, + RotationPolicy: RotationPolicyExtended, + Certificate: *s.signerConfig.Config.Certs[0], }) for _, subCAName := range s.GetSubCANames() { @@ -62,17 +134,139 @@ func (s *CertificateSigner) inventory(path []string) CertificateInventory { for _, certName := range s.GetCertNames() { cert := s.signedCertificates[certName] + meta := cert.GetMeta() certPath := append(append([]string(nil), path...), certName) entries = append(entries, CertificateInventoryEntry{ - Path: certPath, - Role: certificateRole(cert.CSRInfo), - Certificate: *cert.tlsConfig.Certs[0], + Path: certPath, + Name: certName, + Service: meta.Service, + ParentCA: s.signerName, + Role: certificateRole(cert.CSRInfo), + RotationPolicy: certificateRotationPolicy(cert.CSRInfo), + Certificate: *cert.tlsConfig.Certs[0], }) } return entries } +// LoadInventory reads the certificate inventory from disk without creating or +// modifying certificate material. +func (cs *certificateChains) LoadInventory() (CertificateInventory, error) { + signers := append([]CertificateSignerBuilder(nil), cs.signers...) + sort.Slice(signers, func(i, j int) bool { return signers[i].Name() < signers[j].Name() }) + + entries := make(CertificateInventory, 0, len(signers)) + for _, signerBuilder := range signers { + signer, ok := signerBuilder.(*certificateSigner) + if !ok { + return nil, fmt.Errorf("unsupported certificate signer builder %T", signerBuilder) + } + signerEntries, err := signer.loadInventory([]string{signer.Name()}) + if err != nil { + return nil, err + } + entries = append(entries, signerEntries...) + } + return entries, nil +} + +func (s *certificateSigner) loadInventory(path []string) (CertificateInventory, error) { + certificate, err := loadCertificate(cryptomaterial.CACertPath(s.signerDir)) + if err != nil { + return nil, fmt.Errorf("failed to load CA %q: %w", s.signerName, err) + } + + entries := make(CertificateInventory, 0, 1+len(s.subCAs)+len(s.certificatesToSign)) + entries = append(entries, CertificateInventoryEntry{ + Path: append([]string(nil), path...), + Name: s.signerName, + Service: s.service, + ParentCA: parentCA(path), + Role: CertificateRoleCA, + RotationPolicy: RotationPolicyExtended, + Certificate: certificate, + }) + + subCAs := append([]CertificateSignerBuilder(nil), s.subCAs...) + sort.Slice(subCAs, func(i, j int) bool { return subCAs[i].Name() < subCAs[j].Name() }) + for _, subCABuilder := range subCAs { + subCA, ok := subCABuilder.(*certificateSigner) + if !ok { + return nil, fmt.Errorf("unsupported certificate signer builder %T", subCABuilder) + } + subCAPath := append(append([]string(nil), path...), subCA.Name()) + subCAEntries, err := subCA.loadInventory(subCAPath) + if err != nil { + return nil, err + } + entries = append(entries, subCAEntries...) + } + + certificates := append([]CSRInfo(nil), s.certificatesToSign...) + sort.Slice(certificates, func(i, j int) bool { + return certificates[i].GetMeta().Name < certificates[j].GetMeta().Name + }) + for _, info := range certificates { + meta := info.GetMeta() + certificateFilePath, err := certificatePath(s.signerDir, info) + if err != nil { + return nil, err + } + certificate, err := loadCertificate(certificateFilePath) + if err != nil { + return nil, fmt.Errorf("failed to load certificate %q: %w", meta.Name, err) + } + entries = append(entries, CertificateInventoryEntry{ + Path: append(append([]string(nil), path...), meta.Name), + Name: meta.Name, + Service: meta.Service, + ParentCA: s.signerName, + Role: certificateRole(info), + RotationPolicy: certificateRotationPolicy(info), + Certificate: certificate, + }) + } + + return entries, nil +} + +func loadCertificate(path string) (x509.Certificate, error) { + contents, err := os.ReadFile(path) + if err != nil { + return x509.Certificate{}, err + } + certificates, err := crypto.CertsFromPEM(contents) + if err != nil { + return x509.Certificate{}, err + } + if len(certificates) == 0 { + return x509.Certificate{}, fmt.Errorf("no certificates found in %q", path) + } + return *certificates[0], nil +} + +func certificatePath(signerDir string, info CSRInfo) (string, error) { + certificateDir := filepath.Join(signerDir, info.GetMeta().Name) + switch info.(type) { + case *ClientCertificateSigningRequestInfo: + return cryptomaterial.ClientCertPath(certificateDir), nil + case *ServingCertificateSigningRequestInfo: + return cryptomaterial.ServingCertPath(certificateDir), nil + case *PeerCertificateSigningRequestInfo: + return cryptomaterial.PeerCertPath(certificateDir), nil + default: + return "", fmt.Errorf("unsupported certificate request info %T", info) + } +} + +func parentCA(path []string) string { + if len(path) < 2 { + return "" + } + return path[len(path)-2] +} + func certificateRole(info CSRInfo) CertificateRole { switch info.(type) { case *ClientCertificateSigningRequestInfo: @@ -85,3 +279,14 @@ func certificateRole(info CSRInfo) CertificateRole { return CertificateRoleUnknown } } + +func certificateRotationPolicy(info CSRInfo) RotationPolicy { + switch info.(type) { + case *ServingCertificateSigningRequestInfo: + return RotationPolicyStandard + case *ClientCertificateSigningRequestInfo, *PeerCertificateSigningRequestInfo: + return info.GetMeta().RotationPolicy + default: + return RotationPolicyUnknown + } +} diff --git a/pkg/util/cryptomaterial/certchains/inventory_test.go b/pkg/util/cryptomaterial/certchains/inventory_test.go index 839b9186db..c85627d45a 100644 --- a/pkg/util/cryptomaterial/certchains/inventory_test.go +++ b/pkg/util/cryptomaterial/certchains/inventory_test.go @@ -1,10 +1,17 @@ package certchains import ( + "crypto/x509" + "os" + "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/user" + + "github.com/openshift/microshift/pkg/util/cryptomaterial" ) func TestCertificateChains_Inventory(t *testing.T) { @@ -40,13 +47,23 @@ func TestCertificateChains_Inventory(t *testing.T) { for index, want := range expected { entry := inventory[index] require.Equal(t, want.path, strings.Join(entry.Path, "/")) + require.Equal(t, entry.Path[len(entry.Path)-1], entry.Name) require.Equal(t, want.role, entry.Role) require.False(t, entry.Certificate.NotAfter.IsZero()) + if len(entry.Path) > 1 { + require.Equal(t, entry.Path[len(entry.Path)-2], entry.ParentCA) + } else { + require.Empty(t, entry.ParentCA) + } if entry.Role == CertificateRoleCA { + require.Equal(t, RotationPolicyExtended, entry.RotationPolicy) require.NotNil(t, chains.GetSigner(entry.Path...)) continue } + if entry.Role == CertificateRoleServing { + require.Equal(t, RotationPolicyStandard, entry.RotationPolicy) + } _, _, err := chains.GetCertKey(entry.Path...) require.NoError(t, err) } @@ -102,3 +119,126 @@ func TestCertificateInventory_ByRole(t *testing.T) { }) } } + +func TestCertificateInventoryEntry_ZoneAt(t *testing.T) { + now := time.Date(2026, time.September, 8, 10, 30, 0, 0, time.UTC) + tests := []struct { + name string + policy RotationPolicy + notBefore time.Time + notAfter time.Time + want CertificateZone + wantError bool + }{ + { + name: "standard green", + policy: RotationPolicyStandard, + notBefore: now.Add(-4169 * time.Hour), + notAfter: now.Add(5831 * time.Hour), + want: CertificateZoneGreen, + }, + { + name: "standard green boundary is yellow", + policy: RotationPolicyStandard, + notBefore: now.Add(-417 * time.Hour), + notAfter: now.Add(583 * time.Hour), + want: CertificateZoneYellow, + }, + { + name: "standard yellow boundary is red", + policy: RotationPolicyStandard, + notBefore: now.Add(-667 * time.Hour), + notAfter: now.Add(333 * time.Hour), + want: CertificateZoneRed, + }, + { + name: "extended green boundary is yellow", + policy: RotationPolicyExtended, + notBefore: now.Add(-850 * time.Hour), + notAfter: now.Add(150 * time.Hour), + want: CertificateZoneYellow, + }, + { + name: "extended yellow boundary is red", + policy: RotationPolicyExtended, + notBefore: now.Add(-900 * time.Hour), + notAfter: now.Add(100 * time.Hour), + want: CertificateZoneRed, + }, + { + name: "not yet valid", + policy: RotationPolicyStandard, + notBefore: now.Add(time.Hour), + notAfter: now.Add(1000 * time.Hour), + want: CertificateZoneRed, + }, + { + name: "expired", + policy: RotationPolicyStandard, + notBefore: now.Add(-1000 * time.Hour), + notAfter: now, + want: CertificateZoneRed, + }, + { + name: "unknown policy", + policy: RotationPolicyUnknown, + notBefore: now.Add(-500 * time.Hour), + notAfter: now.Add(500 * time.Hour), + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := CertificateInventoryEntry{ + Name: "test-certificate", + RotationPolicy: tt.policy, + Certificate: x509.Certificate{ + NotBefore: tt.notBefore, + NotAfter: tt.notAfter, + }, + } + got, err := entry.ZoneAt(now) + if tt.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestCertificateChainsBuilder_LoadInventory(t *testing.T) { + tmpDir := t.TempDir() + builder := NewCertificateChains( + NewCertificateSigner("test-signer", filepath.Join(tmpDir, "test-signer"), 10*365*24*time.Hour). + WithService("test-service"). + WithClientCertificates(&ClientCertificateSigningRequestInfo{ + CSRMeta: CSRMeta{ + Name: "test-client", + Service: "test-client-service", + Validity: 365 * 24 * time.Hour, + RotationPolicy: RotationPolicyStandard, + }, + UserInfo: &user.DefaultInfo{Name: "test-user"}, + }), + ) + chains, err := builder.Complete() + require.NoError(t, err) + + inventory, err := builder.LoadInventory() + require.NoError(t, err) + require.Equal(t, chains.Inventory(), inventory) + require.Equal(t, "test-service", inventory[0].Service) + require.Equal(t, RotationPolicyExtended, inventory[0].RotationPolicy) + require.Equal(t, "test-client-service", inventory[1].Service) + require.Equal(t, RotationPolicyStandard, inventory[1].RotationPolicy) + + clientCertPath := cryptomaterial.ClientCertPath(filepath.Join(tmpDir, "test-signer", "test-client")) + require.NoError(t, os.Remove(clientCertPath)) + _, err = builder.LoadInventory() + require.Error(t, err) + _, statErr := os.Stat(clientCertPath) + require.ErrorIs(t, statErr, os.ErrNotExist) +} diff --git a/pkg/util/cryptomaterial/certchains/signerbuilder.go b/pkg/util/cryptomaterial/certchains/signerbuilder.go index 4186701151..35c218645e 100644 --- a/pkg/util/cryptomaterial/certchains/signerbuilder.go +++ b/pkg/util/cryptomaterial/certchains/signerbuilder.go @@ -19,6 +19,7 @@ type SignerInfo interface { type CertificateSignerBuilder interface { SignerInfo + WithService(service string) CertificateSignerBuilder WithSignerConfig(config *crypto.CA) CertificateSignerBuilder WithSubCAs(subCAsInfo ...CertificateSignerBuilder) CertificateSignerBuilder WithClientCertificates(signInfos ...*ClientCertificateSigningRequestInfo) CertificateSignerBuilder @@ -30,6 +31,7 @@ type CertificateSignerBuilder interface { type certificateSigner struct { signerName string + service string signerDir string signerValidity time.Duration @@ -56,6 +58,14 @@ func (s *certificateSigner) Name() string { return s.signerName } func (s *certificateSigner) Directory() string { return s.signerDir } func (s *certificateSigner) Validity() time.Duration { return s.signerValidity } +// WithService records the service or function that owns the signer. +// +//nolint:ireturn +func (s *certificateSigner) WithService(service string) CertificateSignerBuilder { + s.service = service + return s +} + // WithSignerConfig uses the provided configuration in `config` to sign its // direct certificates. // This is useful when creating intermediate signers. @@ -116,6 +126,7 @@ func (s *certificateSigner) Complete() (*CertificateSigner, error) { signerCompleted := &CertificateSigner{ signerName: s.signerName, + service: s.service, signerDir: s.signerDir, signerValidity: s.signerValidity, signerConfig: signerConfig, diff --git a/pkg/util/cryptomaterial/certchains/signers.go b/pkg/util/cryptomaterial/certchains/signers.go index 130b706b34..96f86353f3 100644 --- a/pkg/util/cryptomaterial/certchains/signers.go +++ b/pkg/util/cryptomaterial/certchains/signers.go @@ -20,8 +20,10 @@ import ( type CSRInfo interface{ GetMeta() CSRMeta } type CSRMeta struct { - Name string - Validity time.Duration + Name string + Service string + Validity time.Duration + RotationPolicy RotationPolicy } type ClientCertificateSigningRequestInfo struct { @@ -51,6 +53,7 @@ func (i *PeerCertificateSigningRequestInfo) GetMeta() CSRMeta { return i.CSRMeta type CertificateSigner struct { signerName string + service string signerConfig *crypto.CA signerDir string signerValidity time.Duration @@ -230,7 +233,7 @@ func (s *CertificateSigner) AddToBundles(bundlePaths ...string) error { } func (s *CertificateSigner) toBuilder() CertificateSignerBuilder { - signer := NewCertificateSigner(s.signerName, s.signerDir, s.signerValidity) + signer := NewCertificateSigner(s.signerName, s.signerDir, s.signerValidity).WithService(s.service) for _, subCA := range s.subCAs { signer = signer.WithSubCAs(subCA.toBuilder())