From b58d286fcf09a87fc7821bcdfc0618ae4fff9a03 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 5 Dec 2023 13:40:31 -0700 Subject: [PATCH] feat(metadata): Signer recognizes annotation prefix for Command metadata --- .../k8s-csr-signer/templates/clusterrole.yaml | 21 +------ .../k8s-csr-signer/templates/secretrole.yaml | 38 +++++++++++++ docs/annotations.markdown | 9 +++ docs/getting-started.markdown | 55 +++++++++++++++++++ .../certificatesigningrequest_controller.go | 13 ++++- internal/controllers/fake_signer_test.go | 4 ++ internal/signer/signer.go | 49 ++++++++++++++++- internal/signer/signer_test.go | 39 +++++++++++-- sample/sample.yaml | 1 + 9 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 deploy/charts/k8s-csr-signer/templates/secretrole.yaml diff --git a/deploy/charts/k8s-csr-signer/templates/clusterrole.yaml b/deploy/charts/k8s-csr-signer/templates/clusterrole.yaml index 51d945a..59095cc 100644 --- a/deploy/charts/k8s-csr-signer/templates/clusterrole.yaml +++ b/deploy/charts/k8s-csr-signer/templates/clusterrole.yaml @@ -6,25 +6,6 @@ metadata: labels: {{- include "command-csr-signer.labels" . | nindent 4 }} rules: - # The controller needs to be able to read secrets and configmaps to get authentication information - # and to configure itself - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - # configuration validation webhook controller - apiGroups: ["admissionregistration.k8s.io"] resources: ["validatingwebhookconfigurations"] @@ -44,4 +25,4 @@ rules: resourceNames: {{- toYaml . | nindent 6 }} {{- end }} - verbs: ["approve", "sign"] + verbs: ["sign"] diff --git a/deploy/charts/k8s-csr-signer/templates/secretrole.yaml b/deploy/charts/k8s-csr-signer/templates/secretrole.yaml new file mode 100644 index 0000000..897843d --- /dev/null +++ b/deploy/charts/k8s-csr-signer/templates/secretrole.yaml @@ -0,0 +1,38 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "command-csr-signer.labels" . | nindent 4 }} + name: {{ include "command-csr-signer.name" . }}-secret-reader-role +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + {{- include "command-csr-signer.labels" . | nindent 4 }} + name: {{ include "command-csr-signer.name" . }}-secret-reader-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "command-csr-signer.name" . }}-secret-reader-role +subjects: + - kind: ServiceAccount + name: {{ include "command-csr-signer.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/docs/annotations.markdown b/docs/annotations.markdown index b2f5fb2..822ea34 100644 --- a/docs/annotations.markdown +++ b/docs/annotations.markdown @@ -31,6 +31,15 @@ Here are the supported annotations that can override the default values: k8s-csr-signer.keyfactor.com/chainDepth: 3 ``` +### Metadata Annotations + +The Keyfactor Command K8s CSR Signer also allows you to specify Command Metadata through the use of annotations. Metadata attached to a certificate request will be stored in Command and can be used for reporting and auditing purposes. The syntax for specifying metadata is as follows: +```yaml +metadata.k8s-csr-signer.keyfactor.com/: +``` + +Ensure that the metadata specified by the `metadata-field-name` matches a name of a metadata field in Command exactly. If the metadata field name does not match, the CSR enrollment will fail. + ### How to Apply Annotations To apply these annotations, include them in the metadata section of your CertificateSigningRequest resource: diff --git a/docs/getting-started.markdown b/docs/getting-started.markdown index 9c5003d..cc7631a 100644 --- a/docs/getting-started.markdown +++ b/docs/getting-started.markdown @@ -19,6 +19,61 @@ * Helm (to deploy to Kubernetes) * [Helm](https://helm.sh/docs/intro/install/) (v3.1 +) +### Keyfactor Command Configuration +The Command K8s CSR Signer populates metadata fields on issued certificates in Command pertaining to the K8s cluster and CertificateSigningRequest reconcile loops. Before deploying any CSRs in the cluster, these metadata fields must be created in Command. To easily create these metadata fields, use the `kfutil` Keyfactor command line tool that offers convenient and powerful command line access to the Keyfactor platform. Before proceeding, ensure that `kfutil` is installed and configured by following the instructions here: [https://github.com/Keyfactor/kfutil](https://github.com/Keyfactor/kfutil). + +Then, use the `import` command to import the metadata fields into Command: +```shell +cat <> metadata.json +{ + "Collections": [], + "MetadataFields": [ + { + "AllowAPI": true, + "DataType": 1, + "Description": "The reconcile ID of the reconcile loop that signed the certificate.", + "Name": "Controller-Reconcile-Id" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The namespace that the controller reconciler is running in.", + "Name": "Controller-Namespace" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The name of the resource that a K8s controller is reconciling.", + "Name": "Controller-Kind" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The group name of the resource that a K8s controller is reconciling.", + "ExplicitUpdate": false, + "Name": "Controller-Resource-Group-Name" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The name of the resource being reconciled in Kubernetes", + "Name": "Controller-Resource-Name", + } + ], + "ExpirationAlerts": [], + "IssuedCertAlerts": [], + "DeniedCertAlerts": [], + "PendingCertAlerts": [], + "Networks": [], + "WorkflowDefinitions": [], + "BuiltInReports": [], + "CustomReports": [], + "SecurityRoles": [] +} +EOF +kfutil import --metadata --file metadata.json +``` + ## Getting Started Install required software and their dependencies if not already present. Additionally, verify that at least one Kubernetes node is running by running the following command: diff --git a/internal/controllers/certificatesigningrequest_controller.go b/internal/controllers/certificatesigningrequest_controller.go index fcb5145..e3f6222 100644 --- a/internal/controllers/certificatesigningrequest_controller.go +++ b/internal/controllers/certificatesigningrequest_controller.go @@ -30,6 +30,7 @@ import ( "k8s.io/utils/clock" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" ) type CertificateSigningRequestReconciler struct { @@ -45,6 +46,8 @@ type CertificateSigningRequestReconciler struct { func (c *CertificateSigningRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { reconcileLog := ctrl.LoggerFrom(ctx) + meta := signer.K8sMetadata{} + c.SignerBuilder.Reset() // Get the CertificateSigningRequest @@ -116,12 +119,20 @@ func (c *CertificateSigningRequestReconciler) Reconcile(ctx context.Context, req } } + // Populate metadata + meta.ControllerNamespace = c.ClusterResourceNamespace + meta.ControllerKind = "CertificateSigningRequest" + meta.ControllerResourceGroupName = "certificatesigningrequests.certificates.k8s.io" + meta.ControllerReconcileId = string(controller.ReconcileIDFromContext(ctx)) + meta.ControllerResourceName = certificateSigningRequest.GetName() + // Apply the configuration to the signer builder c.SignerBuilder. WithContext(ctx). WithCredsSecret(creds). WithConfigMap(config). - WithCACertConfigMap(root) + WithCACertConfigMap(root). + WithMetadata(meta) // Validate that there were no issues with the configuration err = c.SignerBuilder.PreFlight() diff --git a/internal/controllers/fake_signer_test.go b/internal/controllers/fake_signer_test.go index 6e7f348..285581d 100644 --- a/internal/controllers/fake_signer_test.go +++ b/internal/controllers/fake_signer_test.go @@ -50,6 +50,10 @@ func (f *FakeSignerBuilder) WithCACertConfigMap(configMap corev1.ConfigMap) sign return f } +func (f *FakeSignerBuilder) WithMetadata(meta signer.K8sMetadata) signer.Builder { + return f +} + func (f *FakeSignerBuilder) PreFlight() error { return nil } diff --git a/internal/signer/signer.go b/internal/signer/signer.go index cddf1c9..0cc0f88 100644 --- a/internal/signer/signer.go +++ b/internal/signer/signer.go @@ -23,8 +23,9 @@ var _ Builder = &commandSigner{} var _ Signer = &commandSigner{} const ( - enrollmentPEMFormat = "PEM" - annotationPrefix = "k8s-csr-signer.keyfactor.com/" + enrollmentPEMFormat = "PEM" + annotationPrefix = "k8s-csr-signer.keyfactor.com/" + commandMetadataAnnotationPrefix = "metadata.k8s-csr-signer.keyfactor.com/" ) type Builder interface { @@ -33,6 +34,7 @@ type Builder interface { WithCredsSecret(corev1.Secret) Builder WithConfigMap(corev1.ConfigMap) Builder WithCACertConfigMap(corev1.ConfigMap) Builder + WithMetadata(meta K8sMetadata) Builder PreFlight() error Build() Signer } @@ -41,11 +43,30 @@ type Signer interface { Sign(csr certificates.CertificateSigningRequest) ([]byte, error) } +type K8sMetadata struct { + ControllerNamespace string + ControllerKind string + ControllerResourceGroupName string + ControllerReconcileId string + ControllerResourceName string +} + +const ( + CommandMetaControllerNamespace = "Controller-Namespace" + CommandMetaControllerKind = "Controller-Kind" + CommandMetaControllerResourceGroupName = "Controller-Resource-Group-Name" + CommandMetaControllerReconcileId = "Controller-Reconcile-Id" + CommandMetaControllerResourceName = "Controller-Resource-Name" +) + type commandSigner struct { ctx context.Context logger logr.Logger creds corev1.Secret + // Meta + meta K8sMetadata + // Given from config hostname string defaultCertificateTemplate string @@ -162,6 +183,11 @@ func (s *commandSigner) WithCACertConfigMap(config corev1.ConfigMap) Builder { return s } +func (s *commandSigner) WithMetadata(meta K8sMetadata) Builder { + s.meta = meta + return s +} + func (s *commandSigner) PreFlight() error { var err error @@ -271,6 +297,25 @@ func (s *commandSigner) Sign(csr certificates.CertificateSigningRequest) ([]byte } } + // Construct metadata map + meta := map[string]interface{}{ + CommandMetaControllerNamespace: s.meta.ControllerNamespace, + CommandMetaControllerKind: s.meta.ControllerKind, + CommandMetaControllerResourceGroupName: s.meta.ControllerResourceGroupName, + CommandMetaControllerReconcileId: s.meta.ControllerReconcileId, + CommandMetaControllerResourceName: s.meta.ControllerResourceName, + } + + // Set custom metadata from annotations + for key, value := range annotations { + if strings.HasPrefix(key, commandMetadataAnnotationPrefix) { + meta[strings.TrimPrefix(key, commandMetadataAnnotationPrefix)] = value + } + } + + // Set metadata on enrollment request + enroll.SetMetadata(meta) + // Construct CA name from hostname and logical name var caBuilder strings.Builder if certificateAuthorityHostname != "" { diff --git a/internal/signer/signer_test.go b/internal/signer/signer_test.go index d385129..91c21e7 100644 --- a/internal/signer/signer_test.go +++ b/internal/signer/signer_test.go @@ -193,6 +193,30 @@ func TestCommandSignerBuilder(t *testing.T) { } }) }) + + t.Run("WithMetadata", func(t *testing.T) { + meta := GetFakeMetadata() + + signer.WithContext(ctrl.LoggerInto(context.TODO(), logrtesting.New(t))) + + signer.WithMetadata(meta) + + assert.Equal(t, meta.ControllerNamespace, signer.meta.ControllerNamespace) + assert.Equal(t, meta.ControllerKind, signer.meta.ControllerKind) + assert.Equal(t, meta.ControllerResourceGroupName, signer.meta.ControllerResourceGroupName) + assert.Equal(t, meta.ControllerReconcileId, signer.meta.ControllerReconcileId) + assert.Equal(t, meta.ControllerResourceName, signer.meta.ControllerResourceName) + }) +} + +func GetFakeMetadata() K8sMetadata { + return K8sMetadata{ + ControllerNamespace: "fake-namespace", + ControllerKind: "fake-kind", + ControllerResourceGroupName: "fake-resource-group", + ControllerReconcileId: "fake-reconcile-id", + ControllerResourceName: "fake-resource-name", + } } func TestCommandSigner(t *testing.T) { @@ -238,7 +262,8 @@ func TestCommandSigner(t *testing.T) { WithContext(ctrl.LoggerInto(context.TODO(), logrtesting.New(t))). WithCredsSecret(creds). WithConfigMap(signerConfig). - WithCACertConfigMap(caConfig) + WithCACertConfigMap(caConfig). + WithMetadata(GetFakeMetadata()) err = builder.PreFlight() if err != nil { @@ -281,10 +306,11 @@ func TestCommandSigner(t *testing.T) { // Create supported annotations supportedAnnotations := map[string]string{ - "k8s-csr-signer.keyfactor.com/certificateTemplate": commandConfig.commandCertificateTemplate, - "k8s-csr-signer.keyfactor.com/certificateAuthorityHostname": commandConfig.commandCertificateAuthorityHostname, - "k8s-csr-signer.keyfactor.com/certificateAuthorityLogicalName": commandConfig.commandCertificateAuthorityLogicalName, - "k8s-csr-signer.keyfactor.com/chainDepth": "5", + annotationPrefix + "certificateTemplate": commandConfig.commandCertificateTemplate, + annotationPrefix + "certificateAuthorityHostname": commandConfig.commandCertificateAuthorityHostname, + annotationPrefix + "certificateAuthorityLogicalName": commandConfig.commandCertificateAuthorityLogicalName, + annotationPrefix + "chainDepth": "5", + commandMetadataAnnotationPrefix + "Email-Contact": "k8s-csr-signer@keyfactor.com", } t.Run("BasicAuthWithAnnotations", func(t *testing.T) { @@ -309,7 +335,8 @@ func TestCommandSigner(t *testing.T) { WithContext(ctrl.LoggerInto(context.TODO(), logrtesting.New(t))). WithCredsSecret(estCreds). WithConfigMap(signerConfig). - WithCACertConfigMap(caConfig) + WithCACertConfigMap(caConfig). + WithMetadata(GetFakeMetadata()) err = builder.PreFlight() if err != nil { diff --git a/sample/sample.yaml b/sample/sample.yaml index b029722..eb96a89 100644 --- a/sample/sample.yaml +++ b/sample/sample.yaml @@ -8,6 +8,7 @@ metadata: k8s-csr-signer.keyfactor.com/certificateAuthorityLogicalName: CommandCA1 k8s-csr-signer.keyfactor.com/chainDepth: "0" + metadata.k8s-csr-signer.keyfactor.com/Email-Contact: "demo-command-signer@command.com" spec: request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ25EQ0NBWVFDQVFBd0lqRWdNQjRHQTFVRUF4TVhhemh6TFdOemNpMXphV2R1WlhJdGRHVnpkQzVqYjIwdwpnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFESmcydDI0MjBERzVCZStsM3FSRGE4Cm1rOFdJRDVzaXJJRVh1aEQzMWQ5R3ZqSVJ4bjRqT01SL2Y0ck93MmM2aWNDQVFkUmRNbG5pSFYzM20vb09FekIKTkVLQWpoSWpTTUVERXpsa1d2MGVGcTFnK25RN2c3bUZpVHB0RDhhck9LZlhBM2t3eDU3bURhWkJHTDNPaXlqUAo5Y2Y5NWl2VldDa2JNZk9JV0ZlTVIyRnY3Um1PcEFiaW1JUnBnSWlnd3dROHVzaVRhN3FNcUJBa2R0b2w2MjJKCkxaWUloSGs2WVB1b253MnBoRHNZL1pkY2dLSi9CaGlaTC9ibng3NlJ0WnRWRVF6OSsySmdWTjA2eHJNUVIyUnoKVUdMbGRKb2dIWDQwTUhQYWRMNXdSZkJiaTVQa0VxeTF4RzRwYUFUY29lNVk2YTlRREQ2cFZXeDhjanVpTXJKOQpBZ01CQUFHZ05UQXpCZ2txaGtpRzl3MEJDUTR4SmpBa01DSUdBMVVkRVFRYk1CbUNGMnM0Y3kxamMzSXRjMmxuCmJtVnlMWFJsYzNRdVkyOXRNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJWUnV0dGk3V0lpY3VMdGxFT2UzZzYKYjdQUzI0Q1hadEhxQ2tDVENZWmtkR1NNaXRPcUljajduenFnWTlndWdjYURSVERWcExkTVo4SWtoZGhuR3BZRgo2R25ma0poQmRQNVg0U3JsdGlRUml0YjNQZXJQTm8rU29xbFFXNTl2S3JselRHVng4MldNbjNYc2JLY0VOam9vCktXUjJKTHVvVVNlOGJqdit1T0E4SVlqQW9WZEtGMkNJVCtIZmVZNzVOcnQ1RVlqZ2tzL0JTVmsva05lRXJKM3kKSEthN3JzeUVFQXpxcmY1QmNXM1B5UlU3UitIWmkxZlpudnlzRTA5Nnd3QlVBb01OYmZ5OFFRUlpXV2l1NTJpMwphSko4VUR4SlcyTys0UGF1Q3haVm0yeUQ1UjdzekYrZjMyeXFjT0xOYVNDWndmNmNrYUNEZmphTjRlUzJnM1drCi0tLS0tRU5EIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLQo= usages: