diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb6bcd6..65f622a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,43 +1,213 @@ -name: helm_release +name: Build and Release on: + push: + branches: + - '*' pull_request: branches: - 'v*' types: + # action should run when the pull request is closed + # (regardless of whether it was merged or just closed) - closed + # Make sure the action runs every time new commits are + # pushed to the pull request's branch + - synchronize + +env: + REGISTRY: ghcr.io + jobs: + build: + name: Build Containers + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/arm64 + - linux/amd64 + - linux/s390x + - linux/ppc64le + + permissions: + contents: read + packages: write + + steps: + + - name: Set IMAGE_NAME + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + # Checkout code + # https://github.com/actions/checkout + - name: Checkout code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Set up QEMU + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login to Docker registry + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build and push Docker image with Buildx + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + push: ${{ github.event.pull_request.merged == true }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true + + # Export digest + - name: Export digest + if: github.event.pull_request.merged == true + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + # Upload digest + - name: Upload digest + if: github.event.pull_request.merged == true + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: digests + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + needs: + - build + steps: + - name: Set IMAGE_NAME + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + # Download digests + # https://github.com/actions/download-artifact + - name: Download digests + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: digests + path: /tmp/digests + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Login to Docker registry + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Create manifest list and push + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + helm: runs-on: ubuntu-latest if: github.event.pull_request.merged == true + needs: + - merge steps: - - name: Extract Version Tag - id: extract_version - run: /bin/bash -c 'echo ::set-output name=VERSION::$(echo ${GITHUB_REF##*/} | cut -c2-)' + - name: Set IMAGE_NAME + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} + + # Checkout code + # https://github.com/actions/checkout + - name: Checkout code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + # Extract metadata (tags, labels) to use in Helm chart + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Checkout - uses: actions/checkout@v3 + # Set version from DOCKER_METADATA_OUTPUT_VERSION as environment variable + - name: Set Version + run: | + echo "VERSION=${DOCKER_METADATA_OUTPUT_VERSION:1}" >> $GITHUB_ENV # Change version and appVersion in Chart.yaml to the tag in the closed PR - name: Update Helm App/Chart Version shell: bash run: | - sed -i "s/^version: .*/version: ${{ steps.extract_version.outputs.VERSION }}/g" deploy/charts/command-cert-manager-issuer/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${{ steps.extract_version.outputs.VERSION }}\"/g" deploy/charts/command-cert-manager-issuer/Chart.yaml + sed -i "s/^version: .*/version: ${{ env.VERSION }}/g" deploy/charts/command-cert-manager-issuer/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${{ env.DOCKER_METADATA_OUTPUT_VERSION }}\"/g" deploy/charts/command-cert-manager-issuer/Chart.yaml + + # Setup Helm + # https://github.com/Azure/setup-helm + - name: Install Helm + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + # Helm requires an ident name to be set for chart-releaser to work - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - name: Install Helm - uses: azure/setup-helm@v3 - + # Build and release Helm chart to GitHub Pages + # https://github.com/helm/chart-releaser-action - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.5.0 + uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" with: - pages_branch: gh-pages - charts_dir: deploy/charts - mark_as_latest: true - packages_with_index: true + charts_dir: deploy/charts \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..988a2a7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: test +on: [workflow_dispatch, push, pull_request] +jobs: + build: + name: Build and Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - run: go build -v . + - name: Run linters + uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0 + with: + version: latest + test: + name: Go Test + needs: build + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - name: Set up Go 1.x + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: 'go.mod' + cache: true + - run: go mod download + - env: + COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME: ${{ vars.COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME }} + COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME: ${{ vars.COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME }} + COMMAND_CERTIFICATE_TEMPLATE: ${{ vars.COMMAND_CERTIFICATE_TEMPLATE }} + COMMAND_HOSTNAME: ${{ vars.COMMAND_HOSTNAME }} + COMMAND_USERNAME: ${{ secrets.COMMAND_USERNAME }} + COMMAND_PASSWORD: ${{ secrets.COMMAND_PASSWORD }} + name: Run go test + run: go test -v ./... \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aac1554 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# v1.0.4 + +## Features +* feat(signer): Signer recognizes `metadata.command-issuer.keyfactor.com/: ` annotations on the CertificateRequest resource and uses them to populate certificate metadata in Command. +* feat(release): Container build and release now uses GitHub Actions. + +## Fixes +* fix(helm): CRDs now correspond to correct values for the `command-issuer`. +* fix(helm): Signer Helm Chart now includes a `secureMetrics` value to enable/disable sidecar RBAC container for further protection of the `/metrics` endpoint. +* fix(signer): Signer now returns CA chain bytes instead of appending to the leaf certificate. +* fix(role): Removed permissions for `configmaps` resource types for the `leader-election-role` role. \ No newline at end of file diff --git a/Makefile b/Makefile index e7ebbd4..4f6b288 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,11 @@ # The version which will be reported by the --version argument of each binary # and which will be used as the Docker image tag -VERSION ?= 1.0.3 +VERSION ?= latest # The Docker repository name, overridden in CI. -DOCKER_REGISTRY ?= m8rmclarenkf -DOCKER_IMAGE_NAME ?= command-cert-manager-external-issuer-controller +DOCKER_REGISTRY ?= "" +DOCKER_IMAGE_NAME ?= "" # Image URL to use all building/pushing image targets IMG ?= ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}:${VERSION} -#IMG ?= command-issuer-dev:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.26.0 @@ -67,6 +66,11 @@ test: manifests generate fmt vet envtest ## Run tests. ##@ Build +.PHONY: regcheck +regcheck: ## Check if the docker registry is set. + @test -n "$(DOCKER_REGISTRY)" || (echo "DOCKER_REGISTRY is not set" && exit 1) + @test -n "$(DOCKER_IMAGE_NAME)" || (echo "DOCKER_IMAGE_NAME is not set" && exit 1) + .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager main.go @@ -79,10 +83,10 @@ run: manifests generate fmt vet ## Run a controller from your host. # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build -docker-build: test ## Build docker image with the manager. +docker-build: regcheck ## Build docker image with the manager. docker build -t ${IMG} . -.PHONY: docker-push +.PHONY: docker-push regcheck docker-push: ## Push docker image with the manager. docker push ${IMG} @@ -94,7 +98,7 @@ docker-push: ## Push docker image with the manager. # To properly provided solutions that supports more than one platform you should use this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx -docker-buildx: test ## Build and push docker image for the manager for cross-platform support +docker-buildx: regcheck ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - docker buildx create --name project-v3-builder @@ -122,6 +126,14 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} $(KUSTOMIZE) build config/default | kubectl apply -f - +# Build the manager image for local development. This image is not intended to be used in production. +# Then, install it into the K8s cluster +.PHONY: deploy-local +deploy-local: manifests kustomize ## Build docker image with the manager. + docker build -t command-issuer-dev:latest -f Dockerfile . + cd config/manager && $(KUSTOMIZE) edit set image controller=command-issuer-dev:latest + $(KUSTOMIZE) build config/default | kubectl apply -f - + .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - diff --git a/README.md b/README.md index 91e5ee1..5b27c16 100644 --- a/README.md +++ b/README.md @@ -18,540 +18,8 @@ The cert-manager external issuer for Keyfactor command is open source and commun ###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, see the [contribution guidelines](https://github.com/Keyfactor/command-k8s-csr-signer/blob/main/CONTRIBUTING.md) and use the **[Pull requests](../../pulls)** tab. -## Quick Start - -The quick start guide will walk you through the process of installing the cert-manager external issuer for Keyfactor Command. -The controller image is pulled from [Docker Hub](https://hub.docker.com/r/m8rmclarenkf/command-external-issuer). - -###### To build the container from sources, refer to the [Building Container Image from Source](#building-container-image-from-source) section. - -### Requirements -* [Git](https://git-scm.com/) -* [Make](https://www.gnu.org/software/make/) -* [Docker](https://docs.docker.com/engine/install/) >= v20.10.0 -* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) >= v1.11.3 -* Kubernetes >= v1.19 - * [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), or [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) -* [Keyfactor Command](https://www.keyfactor.com/products/command/) >= v10.1.0 -* [cert-manager](https://cert-manager.io/docs/installation/) >= v1.11.0 -* [cmctl](https://cert-manager.io/docs/reference/cmctl/) - -Before starting, ensure that all of the above requirements are met, and that Keyfactor Command is properly configured. Refer -to the [Keyfactor Configuration](#keyfactor-command-configuration) section for more information. -Additionally, verify that at least one Kubernetes node is running by running the following command: -```shell -kubectl get nodes -``` - -### Installation from Manifests - -Once Kubernetes is running, a static installation of cert-manager can be installed with the following command: -```shell -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml -``` - -###### :pushpin: Running the static cert-manager configuration is not recommended for production use. For more information, see [Installing cert-manager](https://cert-manager.io/docs/installation/). - -Then, install the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command: -```shell -make install -``` - -Finally, deploy the controller to the cluster: -```shell -make deploy -``` - -### Installation from Helm Chart - -The cert-manager external issuer for Keyfactor Command can also be installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). - -First, add the Helm repository: -```bash -helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer -helm repo update -``` - -Then, install the chart: -```bash -helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer -``` - -Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following command: -```bash -helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer --set replicaCount=2 -``` - -## Usage -The cert-manager external issuer for Keyfactor Command can be used to issue certificates from Keyfactor Command using cert-manager. - -### Authentication -Authentication to the Command platform is done using basic authentication. The credentials must be provided as a Kubernetes `kubernetes.io/basic-auth` secret. These credentials should be for a user with "Certificate Enrollment: Enroll CSR" and "API: Read" permissions in Command. - -Create a `kubernetes.io/basic-auth` secret with the Keyfactor Command username and password: -```shell -cat < - password: -EOF -``` - -If the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root, the CA certificate must be provided as a Kubernetes secret. -```shell -kubectl -n command-issuer-system create secret generic command-ca-secret --from-file=ca.crt -``` - -### Creating Issuer and ClusterIssuer resources -The `command-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. -The Command controller will automatically detect and process resources of both types. - -The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. -For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. - -The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: -* `hostname` - The hostname of the Keyfactor Command server -* `commandSecretName` - The name of the Kubernetes `kubernetes.io/basic-auth` secret containing credentials to the Keyfactor instance -* `certificateTemplate` - The short name corresponding to a template in Command that will be used to issue certificates. -* `certificateAuthorityLogicalName` - The logical name of the CA to use to sign the certificate request -* `certificateAuthorityHostname` - The CAs hostname to use to sign the certificate request -* `caSecretName` - The name of the Kubernetes secret containing the CA certificate. This field is optional and only required if the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root. - -###### If a different combination of hostname/certificate authority/certificate profile/end entity profile is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. - -The following is an example of an Issuer resource: -```shell -cat <> command-issuer.yaml -apiVersion: command-issuer.keyfactor.com/v1alpha1 -kind: Issuer -metadata: - labels: - app.kubernetes.io/name: issuer - app.kubernetes.io/instance: issuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer -name: issuer-sample -spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" -EOF -kubectl -n command-issuer-system apply -f command-issuer.yaml -``` - -###### :pushpin: Issuers can only issue certificates in the same namespace as the issuer resource. To issue certificates in multiple namespaces, use a ClusterIssuer. - -The following is an example of a ClusterIssuer resource: -```shell -cat <> command-clusterissuer.yaml -apiVersion: command-issuer.keyfactor.com/v1alpha1 -kind: ClusterIssuer -metadata: - labels: - app.kubernetes.io/name: clusterissuer - app.kubernetes.io/instance: clusterissuer-sample - app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/created-by: command-issuer - name: clusterissuer-sample -spec: - hostname: "" - commandSecretName: "" - certificateTemplate: "" - certificateAuthorityLogicalName: "" - certificateAuthorityHostname: "" - caSecretName: "" -EOF -kubectl -n command-issuer-system apply -f command-clusterissuer.yaml -``` - -###### :pushpin: ClusterIssuers can issue certificates in any namespace. To issue certificates in a single namespace, use an Issuer. - -To create new resources from the above examples, replace the empty strings with the appropriate values and apply the resources to the cluster: -```shell -kubectl -n command-issuer-system apply -f issuer.yaml -kubectl -n command-issuer-system apply -f clusterissuer.yaml -``` - -### Using Issuer and ClusterIssuer resources -Once the Issuer and ClusterIssuer resources are created, they can be used to issue certificates using cert-manager. -The two most important concepts are `Certificate` and `CertificateRequest` resources. `Certificate` -resources represent a single X.509 certificate and its associated attributes, and automatically renews the certificate -and keeps it up to date. When `Certificate` resources are created, they create `CertificateRequest` resources, which -use an Issuer or ClusterIssuer to actually issue the certificate. - -###### To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). - -The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, -and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a -Kubernetes secret named `command-certificate`. -```yaml -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: command-certificate -spec: - commonName: command-issuer-sample - secretName: command-certificate - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer -``` - -###### :pushpin: Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. - -###### :pushpin: Since this certificate request called `command-certificate` is configured to use `issuer-sample`, it must be deployed in the same namespace as `issuer-sample`. - -Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. -```yaml -apiVersion: cert-manager.io/v1 -kind: CertificateRequest -metadata: - name: command-certificate -spec: - request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2REQ0NBVndDQVFBd0x6RUxNQWtHQTFVRUN4TUNTVlF4SURBZUJnTlZCQU1NRjJWcVltTmhYM1JsY25KaApabTl5YlY5MFpYTjBZV05qTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4blNSCklqZDZSN2NYdUNWRHZscXlFcUhKalhIazljN21pNTdFY3A1RXVnblBXa0YwTHBVc25PMld6WTE1bjV2MHBTdXMKMnpYSURhS3NtZU9ZQzlNOWtyRjFvOGZBelEreHJJWk5SWmg0cUZXRmpyNFV3a0EySTdUb05veitET2lWZzJkUgo1cnNmaFdHMmwrOVNPT3VscUJFcWVEcVROaWxyNS85OVpaemlBTnlnL2RiQXJibWRQQ1o5OGhQLzU0NDZhci9NCjdSd2ludjVCMnNRcWM0VFZwTTh3Nm5uUHJaQXA3RG16SktZbzVOQ3JyTmw4elhIRGEzc3hIQncrTU9DQUw0T00KTkJuZHpHSm5KenVyS0c3RU5UT3FjRlZ6Z3liamZLMktyMXRLS3pyVW5keTF1bTlmTWtWMEZCQnZ0SGt1ZG0xdwpMUzRleW1CemVtakZXQi9yRVFJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUJhdFpIVTdOczg2Cmgxc1h0d0tsSi95MG1peG1vRWJhUTNRYXAzQXVFQ2x1U09mdjFDZXBQZjF1N2dydEp5ZGRha1NLeUlNMVNzazAKcWNER2NncUsxVVZDR21vRkp2REZEaEUxMkVnM0ZBQ056UytFNFBoSko1N0JBSkxWNGZaeEpZQ3JyRDUxWnk3NgpPd01ORGRYTEVib0w0T3oxV3k5ZHQ3bngyd3IwWTNZVjAyL2c0dlBwaDVzTHl0NVZOWVd6eXJTMzJYckJwUWhPCnhGMmNNUkVEMUlaRHhuMjR2ZEtINjMzSFo1QXd0YzRYamdYQ3N5VW5mVUE0ZjR1cHBEZWJWYmxlRFlyTW1iUlcKWW1NTzdLTjlPb0MyZ1lVVVpZUVltdHlKZTJkYXlZSHVyUUlpK0ZsUU5zZjhna1hYeG45V2drTnV4ZTY3U0x5dApVNHF4amE4OCs1ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0t - issuerRef: - name: issuer-sample - group: command-issuer.keyfactor.com - kind: Issuer -``` - -### Approving Certificate Requests -Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources -will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using -[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example -of approving a CertificateRequest resource named `command-certificate` in the `command-issuer-system` namespace. -```shell -cmctl -n command-issuer-system approve ejbca-certificate -``` - -Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the -CertificateRequest resource. The following is an example of retrieving the certificate from the secret. -```shell -kubectl get secret command-certificate -n command-issuer-system -o jsonpath='{.data.tls\.crt}' | base64 -d -``` - -###### To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). - -###### :pushpin: If the certificate was issued successfully, the Approved and Ready field will both be set to `True`. - -## Annotation Overrides for Issuer and ClusterIssuer Resources -The Keyfactor Command external issuer for cert-manager allows you to override default settings in the Issuer and ClusterIssuer resources through the use of annotations. This gives you more granular control on a per-Certificate/CertificateRequest basis. - -### Supported Annotations -Here are the supported annotations that can override the default values: - -- **`command-issuer.keyfactor.com/certificateTemplate`**: Overrides the `certificateTemplate` field from the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" - ``` - -- **`command-issuer.keyfactor.com/certificateAuthorityLogicalName`**: Specifies the Certificate Authority (CA) logical name to use, overriding the default CA specified in the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" - ``` - -- **`command-issuer.keyfactor.com/certificateAuthorityHostname`**: Specifies the Certificate Authority (CA) hostname to use, overriding the default CA specified in the resource spec. - - ```yaml - command-issuer.keyfactor.com/certificateAuthorityHostname: "example.com" - ``` - -### How to Apply Annotations - -To apply these annotations, include them in the metadata section of your CertificateRequest resource: - -```yaml -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - annotations: - command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" - command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" - # ... other annotations -spec: -# ... rest of the spec -``` - -### Demo ClusterIssuer Usage with K8s Ingress -This demo will show how to use a ClusterIssuer to issue a certificate for an Ingress resource. The demo uses the Kubernetes -`ingress-nginx` Ingress controller. If Minikube is being used, run the following command to enable the controller. -```shell -minikube addons enable ingress -kubectl get pods -n ingress-nginx -``` - -To manually deploy `ingress-nginx`, run the following command: -```shell -kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml -``` - -Create a namespace for the demo: -```shell -kubectl create ns command-clusterissuer-demo -``` - -Deploy two Pods running the `hashicorp/http-echo` image: -```shell -cat < -``` - -Validate that the certificate was created: -```shell -kubectl -n command-clusterissuer-demo describe ingress command-ingress-demo -``` - -Test it out -```shell -curl -k https://localhost/apple -curl -k https://localhost/banana -``` - -Clean up -```shell -kubectl -n command-clusterissuer-demo delete ingress command-ingress-demo -kubectl -n command-clusterissuer-demo delete service apple-service banana-service -kubectl -n command-clusterissuer-demo delete pod apple-app banana-app -kubectl delete ns command-clusterissuer-demo -kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml -``` - -## Cleanup -To list the certificates and certificate requests created, run the following commands: -```shell -kubectl get certificates -n command-issuer-system -kubectl get certificaterequests -n command-issuer-system -``` - -To remove the certificate and certificate request resources, run the following commands: -```shell -kubectl delete certificate command-certificate -n command-issuer-system -kubectl delete certificaterequest command-certificate -n command-issuer-system -``` - -To list the issuer and cluster issuer resources created, run the following commands: -```shell -kubectl -n command-issuer-system get issuers.command-issuer.keyfactor.com -kubectl -n command-issuer-system get clusterissuers.command-issuer.keyfactor.com -``` - -To remove the issuer and cluster issuer resources, run the following commands: -```shell -kubectl -n command-issuer-system delete issuers.command-issuer.keyfactor.com -kubectl -n command-issuer-system delete clusterissuers.command-issuer.keyfactor.com -``` - -To remove the controller from the cluster, run: -```shell -make undeploy -``` - -To remove the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command, run: -```shell -make uninstall -``` - -## Keyfactor Command Configuration -The Command Issuer for cert-manager populates metadata fields in Command pertaining to the K8s cluster and cert-manager Issuer/ClusterIssuer. -Before configuring the issuer, create these metadata fields. These fields will be populated using 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) - -Use the `import` command to import the metadata fields into Command: -```shell -cat <> metadata.json -{ - "Collections": [], - "MetadataFields": [ - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the issuer resource was created in.", - "Name": "Issuer-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The certificate reconcile ID that the controller used to issue this certificate.", - "Name": "Controller-Reconcile-Id" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the CertificateSigningRequest resource was created in.", - "Name": "Certificate-Signing-Request-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The namespace that the controller container is running in.", - "Name": "Controller-Namespace" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The type of issuer that the controller used to issue this certificate.", - "Name": "Controller-Kind" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The group name of the resource that the Issuer or ClusterIssuer controller is managing.", - "Name": "Controller-Resource-Group-Name" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The name of the K8s issuer resource", - "Name": "Issuer-Name" - }, - { - "AllowAPI": true, - "DataType": 1, - "Description": "The name of the K8s issuer resource", - "Name": "Issuer-Name" - } - ], - "ExpirationAlerts": [], - "IssuedCertAlerts": [], - "DeniedCertAlerts": [], - "PendingCertAlerts": [], - "Networks": [], - "WorkflowDefinitions": [], - "BuiltInReports": [], - "CustomReports": [], - "SecurityRoles": [] -} -kfutil import --metadata --file metadata.json -``` - -## Building Container Image from Source - -### Requirements -* [Golang](https://golang.org/) >= v1.19 - -Building the container from source first runs appropriate test cases, which requires all requirements also listed in the -Quick Start section. As part of this testing is an enrollment of a certificate with Command, so a running instance of Command -is also required. - -The following environment variables must be exported before building the container image: -* `COMMAND_HOSTNAME` - The hostname of the Command server to use for testing. -* `COMMAND_USERNAME` - The username of an authorized Command user to use for testing. -* `COMMAND_PASSWORD` - The password of the authorized Command user to use for testing. -* `COMMAND_CERTIFICATE_TEMPLATE` - The name of the certificate template to use for testing. -* `COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME` - The logical name of the certificate authority to use for testing. -* `COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME` - The hostname of the certificate authority to use for testing. -* `COMMAND_CA_CERT_PATH` - A relative or absolute path to the CA certificate that the Command server uses for TLS. The file must include the certificate in PEM format. - -To build the cert-manager external issuer for Keyfactor Command, run: -```shell -make docker-build -``` +* [Installation](docs/install.markdown) +* [Usage](docs/config_usage.markdown) +* [Example Usage](docs/example.markdown) +* [Customization](docs/annotations.markdown) +* [Testing the Source](docs/testing.markdown) diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index 1552b6f..4ed0944 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -22,9 +22,10 @@ import ( // IssuerSpec defines the desired state of Issuer type IssuerSpec struct { - // Hostname is the hostname of the Keyfactor server + // Hostname is the hostname of a Keyfactor Command instance. Hostname string `json:"hostname,omitempty"` - // CertificateTemplate is the name of the certificate template to use + // CertificateTemplate is the name of the certificate template to use. + // Refer to the Keyfactor Command documentation for more information. CertificateTemplate string `json:"certificateTemplate,omitempty"` // CertificateAuthorityLogicalName is the logical name of the certificate authority to use // E.g. "Keyfactor Root CA" or "Intermediate CA" @@ -33,20 +34,19 @@ type IssuerSpec struct { // CertificateAuthorityLogicalName E.g. "ca.example.com" CertificateAuthorityHostname string `json:"certificateAuthorityHostname,omitempty"` - // A reference to a Secret in the same namespace as the referent. If the + // A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth + // credentials for the Command instance configured in Hostname. The secret must + // be in the same namespace as the referent. If the // referent is a ClusterIssuer, the reference instead refers to the resource // with the given name in the configured 'cluster resource namespace', which // is set as a flag on the controller component (and defaults to the - // namespace that the controller runs in). The secret must be a K8s basic-auth - // secret of type kubernetes.io/basic-auth with the username and password - // fields set. + // namespace that the controller runs in). SecretName string `json:"commandSecretName,omitempty"` - // A reference to a Secret in the same namespace as the referent. If the - // referent is a ClusterIssuer, the reference instead refers to the resource - // with the given name in the configured 'cluster resource namespace', which - // is set as a flag on the controller component (and defaults to the - // namespace that the controller runs in). + // The name of the secret containing the CA bundle to use when verifying + // Command's server certificate. If specified, the CA bundle will be added to + // the client trust roots for the Command issuer. + // +optional CaSecretName string `json:"caSecretName"` } diff --git a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml index c2ee40b..217acfe 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml @@ -36,11 +36,9 @@ spec: description: IssuerSpec defines the desired state of Issuer properties: caSecretName: - description: A reference to a Secret in the same namespace as the - referent. If the referent is a ClusterIssuer, the reference instead - refers to the resource with the given name in the configured 'cluster - resource namespace', which is set as a flag on the controller component - (and defaults to the namespace that the controller runs in). + description: The name of the secret containing the CA bundle to use + when verifying Command's server certificate. If specified, the CA + bundle will be added to the client trust roots for the Command issuer. type: string certificateAuthorityHostname: description: CertificateAuthorityHostname is the hostname associated @@ -54,22 +52,20 @@ spec: type: string certificateTemplate: description: CertificateTemplate is the name of the certificate template - to use + to use. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: - description: A reference to a Secret in the same namespace as the - referent. If the referent is a ClusterIssuer, the reference instead - refers to the resource with the given name in the configured 'cluster - resource namespace', which is set as a flag on the controller component - (and defaults to the namespace that the controller runs in). The - secret must be a K8s basic-auth secret of type kubernetes.io/basic-auth - with the username and password fields set. + description: A reference to a K8s kubernetes.io/basic-auth Secret + containing basic auth credentials for the Command instance configured + in Hostname. The secret must be in the same namespace as the referent. + If the referent is a ClusterIssuer, the reference instead refers + to the resource with the given name in the configured 'cluster resource + namespace', which is set as a flag on the controller component (and + defaults to the namespace that the controller runs in). type: string hostname: - description: Hostname is the hostname of the Keyfactor server + description: Hostname is the hostname of a Keyfactor Command instance. type: string - required: - - caSecretName type: object status: description: IssuerStatus defines the observed state of Issuer diff --git a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml index 8bd788b..ffcc231 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml @@ -36,11 +36,9 @@ spec: description: IssuerSpec defines the desired state of Issuer properties: caSecretName: - description: A reference to a Secret in the same namespace as the - referent. If the referent is a ClusterIssuer, the reference instead - refers to the resource with the given name in the configured 'cluster - resource namespace', which is set as a flag on the controller component - (and defaults to the namespace that the controller runs in). + description: The name of the secret containing the CA bundle to use + when verifying Command's server certificate. If specified, the CA + bundle will be added to the client trust roots for the Command issuer. type: string certificateAuthorityHostname: description: CertificateAuthorityHostname is the hostname associated @@ -54,22 +52,20 @@ spec: type: string certificateTemplate: description: CertificateTemplate is the name of the certificate template - to use + to use. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: - description: A reference to a Secret in the same namespace as the - referent. If the referent is a ClusterIssuer, the reference instead - refers to the resource with the given name in the configured 'cluster - resource namespace', which is set as a flag on the controller component - (and defaults to the namespace that the controller runs in). The - secret must be a K8s basic-auth secret of type kubernetes.io/basic-auth - with the username and password fields set. + description: A reference to a K8s kubernetes.io/basic-auth Secret + containing basic auth credentials for the Command instance configured + in Hostname. The secret must be in the same namespace as the referent. + If the referent is a ClusterIssuer, the reference instead refers + to the resource with the given name in the configured 'cluster resource + namespace', which is set as a flag on the controller component (and + defaults to the namespace that the controller runs in). type: string hostname: - description: Hostname is the hostname of the Keyfactor server + description: Hostname is the hostname of a Keyfactor Command instance. type: string - required: - - caSecretName type: object status: description: IssuerStatus defines the observed state of Issuer diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 6badba7..ace19ce 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: m8rmclarenkf/command-external-issuer - newTag: v1.0.2 + newName: ghcr.io/keyfactor/command-cert-manager-issuer + newTag: latest diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml index 9028e07..bbeff7a 100644 --- a/config/rbac/leader_election_role.yaml +++ b/config/rbac/leader_election_role.yaml @@ -11,18 +11,6 @@ metadata: app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - apiGroups: - coordination.k8s.io resources: diff --git a/config/samples/certificate.yaml b/config/samples/certificate.yaml index 23a389a..4a11be7 100644 --- a/config/samples/certificate.yaml +++ b/config/samples/certificate.yaml @@ -2,6 +2,10 @@ apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: command-certificate + annotations: + command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" + command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" + metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" spec: commonName: command-issuer-sample secretName: command-certificate diff --git a/config/samples/command-issuer_v1alpha1_clusterissuer.yaml b/config/samples/command-issuer_v1alpha1_clusterissuer.yaml index ab2ff76..4cce43e 100644 --- a/config/samples/command-issuer_v1alpha1_clusterissuer.yaml +++ b/config/samples/command-issuer_v1alpha1_clusterissuer.yaml @@ -13,3 +13,4 @@ spec: certificateTemplate: "" certificateAuthorityLogicalName: "" certificateAuthorityHostname: "" + caSecretName: "" diff --git a/config/samples/command-issuer_v1alpha1_issuer.yaml b/config/samples/command-issuer_v1alpha1_issuer.yaml index c1307df..faa4d88 100644 --- a/config/samples/command-issuer_v1alpha1_issuer.yaml +++ b/config/samples/command-issuer_v1alpha1_issuer.yaml @@ -5,8 +5,12 @@ metadata: app.kubernetes.io/name: issuer app.kubernetes.io/instance: issuer-sample app.kubernetes.io/part-of: command-issuer - app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: command-issuer name: issuer-sample spec: - # TODO(user): Add fields here + hostname: "" + commandSecretName: "" + certificateTemplate: "" + certificateAuthorityLogicalName: "" + certificateAuthorityHostname: "" + caSecretName: "" diff --git a/deploy/charts/command-cert-manager-issuer/README.md b/deploy/charts/command-cert-manager-issuer/README.md index 1ae5f8f..e992379 100644 --- a/deploy/charts/command-cert-manager-issuer/README.md +++ b/deploy/charts/command-cert-manager-issuer/README.md @@ -31,30 +31,43 @@ helm install command-cert-manager-issuer command-issuer/command-cert-manager-iss Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following command: ```bash -helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer --set replicaCount=2 +helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --set replicaCount=2 +``` + +Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: +```yaml +cat < override.yaml +replicaCount: 2 +EOF +``` +Then, use the `-f` flag to specify the `values.yaml` file: +```bash +helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + -f override.yaml ``` ## Configuration The following table lists the configurable parameters of the `command-cert-manager-issuer` chart and their default values. -| Parameter | Description | Default | -|-----------------------------------|-------------------------------------------------------|----------------------------------------------------------------| -| `replicaCount` | Number of replica command-cert-manager-issuers to run | `1` | -| `image.repository` | Image repository | `m8rmclarenkf/command-cert-manager-external-issuer-controller` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | -| `image.tag` | Image tag | `1.0.3` | -| `imagePullSecrets` | Image pull secrets | `[]` | -| `nameOverride` | Name override | `""` | -| `fullnameOverride` | Full name override | `""` | -| `crd.create` | Specifies if CRDs will be created | `true` | -| `crd.annotations` | Annotations to add to the CRD | `{}` | -| `serviceAccount.create` | Specifies if a service account should be created | `true` | -| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | -| `serviceAccount.name` | Name of the service account to use | `""` (uses the fullname template if `create` is true) | -| `podAnnotations` | Annotations for the pod | `{}` | -| `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | -| `securityContext` | Security context for the pod | `{}` (with commented out options) | -| `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | -| `nodeSelector` | Node labels for pod assignment | `{}` | -| `tolerations` | Tolerations for pod assignment | `[]` | +| Parameter | Description | Default | +|-----------------------------------|-------------------------------------------------------|-------------------------------------------------------| +| `replicaCount` | Number of replica command-cert-manager-issuers to run | `1` | +| `image.repository` | Image repository | `ghcr.io/keyfactor/command-cert-manager-issuer` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag | `""` | +| `imagePullSecrets` | Image pull secrets | `[]` | +| `nameOverride` | Name override | `""` | +| `fullnameOverride` | Full name override | `""` | +| `crd.create` | Specifies if CRDs will be created | `true` | +| `crd.annotations` | Annotations to add to the CRD | `{}` | +| `serviceAccount.create` | Specifies if a service account should be created | `true` | +| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | +| `serviceAccount.name` | Name of the service account to use | `""` (uses the fullname template if `create` is true) | +| `podAnnotations` | Annotations for the pod | `{}` | +| `podSecurityContext.runAsNonRoot` | Run pod as non-root | `true` | +| `securityContext` | Security context for the pod | `{}` (with commented out options) | +| `resources` | CPU/Memory resource requests/limits | `{}` (with commented out options) | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | Tolerations for pod assignment | `[]` | diff --git a/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml b/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml index 0a238b9..c65a41b 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/clusterrole.yaml @@ -53,6 +53,7 @@ rules: - issuers/finalizers verbs: - update +{{- if .Values.secureMetrics.enabled }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -84,4 +85,5 @@ rules: - nonResourceURLs: - /metrics verbs: - - get \ No newline at end of file + - get +{{- end }} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml b/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml index 391282b..8a9c2b6 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/clusterrolebinding.yaml @@ -12,6 +12,7 @@ subjects: - kind: ServiceAccount name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} namespace: {{ .Release.Namespace }} +{{- if .Values.secureMetrics.enabled }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -26,4 +27,5 @@ roleRef: subjects: - kind: ServiceAccount name: {{ include "command-cert-manager-issuer.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} \ No newline at end of file + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml index 5d08de0..f845fda 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -32,30 +32,24 @@ spec: spec: description: IssuerSpec defines the desired state of Issuer properties: - caBundleSecretName: - description: The name of the secret containing the CA bundle to use when verifying command's server certificate. If specified, the CA bundle will be added to the client trust roots for the command issuer. + caSecretName: + description: The name of the secret containing the CA bundle to use when verifying Command's server certificate. If specified, the CA bundle will be added to the client trust roots for the Command issuer. type: string - certificateAuthorityName: + certificateAuthorityHostname: + description: CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by CertificateAuthorityLogicalName E.g. "ca.example.com" type: string - certificateProfileName: + certificateAuthorityLogicalName: + description: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string - commandSecretName: - description: A reference to a Secret in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster resource namespace', which is set as a flag on the controller component (and defaults to the namespace that the controller runs in). - type: string - endEntityName: - description: 'Optional field that overrides the default for how the command issuer should determine the name of the end entity to reference or create when signing certificates. The options are: * cn: Use the CommonName from the CertificateRequest''s DN * dns: Use the first DNSName from the CertificateRequest''s DNSNames SANs * uri: Use the first URI from the CertificateRequest''s URI Sans * ip: Use the first IPAddress from the CertificateRequest''s IPAddresses SANs * certificateName: Use the value of the CertificateRequest''s certificateName annotation If none of the above options are used but endEntityName is populated, the value of endEntityName will be used as the end entity name. If endEntityName is not populated, the default tree listed in the command documentation will be used.' + certificateTemplate: + description: CertificateTemplate is the name of the certificate template to use. Refer to the Keyfactor Command documentation for more information. type: string - endEntityProfileName: + commandSecretName: + description: A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth credentials for the Command instance configured in Hostname. The secret must be in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster resource namespace', which is set as a flag on the controller component (and defaults to the namespace that the controller runs in). type: string hostname: - description: Hostname is the hostname of the command server + description: Hostname is the hostname of a Keyfactor Command instance. type: string - required: - - certificateAuthorityName - - certificateProfileName - - commandSecretName - - endEntityProfileName - - hostname type: object status: description: IssuerStatus defines the observed state of Issuer diff --git a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml index 73012cc..de8de0b 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml @@ -32,30 +32,24 @@ spec: spec: description: IssuerSpec defines the desired state of Issuer properties: - caBundleSecretName: - description: The name of the secret containing the CA bundle to use when verifying command's server certificate. If specified, the CA bundle will be added to the client trust roots for the command issuer. + caSecretName: + description: The name of the secret containing the CA bundle to use when verifying Command's server certificate. If specified, the CA bundle will be added to the client trust roots for the Command issuer. type: string - certificateAuthorityName: + certificateAuthorityHostname: + description: CertificateAuthorityHostname is the hostname associated with the Certificate Authority specified by CertificateAuthorityLogicalName E.g. "ca.example.com" type: string - certificateProfileName: + certificateAuthorityLogicalName: + description: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string - commandSecretName: - description: A reference to a Secret in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster resource namespace', which is set as a flag on the controller component (and defaults to the namespace that the controller runs in). - type: string - endEntityName: - description: 'Optional field that overrides the default for how the command issuer should determine the name of the end entity to reference or create when signing certificates. The options are: * cn: Use the CommonName from the CertificateRequest''s DN * dns: Use the first DNSName from the CertificateRequest''s DNSNames SANs * uri: Use the first URI from the CertificateRequest''s URI Sans * ip: Use the first IPAddress from the CertificateRequest''s IPAddresses SANs * certificateName: Use the value of the CertificateRequest''s certificateName annotation If none of the above options are used but endEntityName is populated, the value of endEntityName will be used as the end entity name. If endEntityName is not populated, the default tree listed in the command documentation will be used.' + certificateTemplate: + description: CertificateTemplate is the name of the certificate template to use. Refer to the Keyfactor Command documentation for more information. type: string - endEntityProfileName: + commandSecretName: + description: A reference to a K8s kubernetes.io/basic-auth Secret containing basic auth credentials for the Command instance configured in Hostname. The secret must be in the same namespace as the referent. If the referent is a ClusterIssuer, the reference instead refers to the resource with the given name in the configured 'cluster resource namespace', which is set as a flag on the controller component (and defaults to the namespace that the controller runs in). type: string hostname: - description: Hostname is the hostname of the command server + description: Hostname is the hostname of a Keyfactor Command instance. type: string - required: - - certificateAuthorityName - - certificateProfileName - - commandSecretName - - endEntityProfileName - - hostname type: object status: description: IssuerStatus defines the observed state of Issuer diff --git a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml index cbc5763..4eb16ce 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/deployment.yaml @@ -26,6 +26,7 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: + {{- if .Values.secureMetrics.enabled }} - args: - --secure-listen-address=0.0.0.0:8443 - --upstream=http://127.0.0.1:8080/ @@ -49,6 +50,7 @@ spec: capabilities: drop: - ALL + {{- end }} - args: - --health-probe-bind-address=:8081 - --metrics-bind-address=127.0.0.1:8080 diff --git a/deploy/charts/command-cert-manager-issuer/templates/role.yaml b/deploy/charts/command-cert-manager-issuer/templates/role.yaml index bd3d437..78b9b28 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/role.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/role.yaml @@ -5,18 +5,6 @@ metadata: {{- include "command-cert-manager-issuer.labels" . | nindent 4 }} name: {{ include "command-cert-manager-issuer.name" . }}-leader-election-role rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - apiGroups: - coordination.k8s.io resources: diff --git a/deploy/charts/command-cert-manager-issuer/templates/service.yaml b/deploy/charts/command-cert-manager-issuer/templates/service.yaml index 6f4f739..c07551c 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/service.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/service.yaml @@ -1,3 +1,4 @@ +{{- if .Values.secureMetrics.enabled }} apiVersion: v1 kind: Service metadata: @@ -11,4 +12,5 @@ spec: protocol: TCP targetPort: https selector: - {{- include "command-cert-manager-issuer.selectorLabels" . | nindent 4 }} \ No newline at end of file + {{- include "command-cert-manager-issuer.selectorLabels" . | nindent 4 }} +{{- end}} \ No newline at end of file diff --git a/deploy/charts/command-cert-manager-issuer/values.yaml b/deploy/charts/command-cert-manager-issuer/values.yaml index 1130491..a521500 100644 --- a/deploy/charts/command-cert-manager-issuer/values.yaml +++ b/deploy/charts/command-cert-manager-issuer/values.yaml @@ -4,7 +4,7 @@ replicaCount: 1 image: - repository: m8rmclarenkf/command-cert-manager-external-issuer-controller + repository: "" pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" @@ -13,6 +13,11 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Whether to enable and configure the kube-rbac-proxy sidecar for authorized and authenticated +# use of the /metrics endpoint by Prometheus. +secureMetrics: + enabled: false + crd: # Specifies whether CRDs will be created create: true diff --git a/docs/annotations.markdown b/docs/annotations.markdown new file mode 100644 index 0000000..afa9a63 --- /dev/null +++ b/docs/annotations.markdown @@ -0,0 +1,63 @@ + + Terraform logo + + +# Annotation Overrides for Issuer and ClusterIssuer Resources + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +The Keyfactor Command external issuer for cert-manager allows you to override default settings in the Issuer and ClusterIssuer resources through the use of annotations. This gives you more granular control on a per-Certificate/CertificateRequest basis. + +### Documentation Tree +* [Installation](install.markdown) +* [Usage](config_usage.markdown) +* [Example Usage](example.markdown) +* [Testing the Source](testing.markdown) + +### Supported Annotations +Here are the supported annotations that can override the default values: + +- **`command-issuer.keyfactor.com/certificateTemplate`**: Overrides the `certificateTemplate` field from the resource spec. + + ```yaml + command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" + ``` + +- **`command-issuer.keyfactor.com/certificateAuthorityLogicalName`**: Specifies the Certificate Authority (CA) logical name to use, overriding the default CA specified in the resource spec. + + ```yaml + command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" + ``` + +- **`command-issuer.keyfactor.com/certificateAuthorityHostname`**: Specifies the Certificate Authority (CA) hostname to use, overriding the default CA specified in the resource spec. + + ```yaml + command-issuer.keyfactor.com/certificateAuthorityHostname: "example.com" + ``` + +### Metadata Annotations + +The Keyfactor Command external issuer for cert-manager 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.command-issuer.keyfactor.com/: +``` + +###### :pushpin: The metadata field name must match 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 CertificateRequest resource: + +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" + command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" + metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" + # ... other annotations +spec: +# ... the rest of the spec +``` \ No newline at end of file diff --git a/docs/config_usage.markdown b/docs/config_usage.markdown new file mode 100644 index 0000000..d5fbc73 --- /dev/null +++ b/docs/config_usage.markdown @@ -0,0 +1,242 @@ + + Terraform logo + + +# Command Cert Manager Issuer Usage + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +The cert-manager external issuer for Keyfactor Command can be used to issue certificates from Keyfactor Command using cert-manager. + +### Documentation Tree +* [Installation](install.markdown) +* [Example Usage](example.markdown) +* [Customization](annotations.markdown) +* [Testing the Source](testing.markdown) + +### Keyfactor Command Configuration +The Command Issuer for cert-manager populates metadata fields on issued certificates in Command pertaining to the K8s cluster and cert-manager Issuer/ClusterIssuer. Before deploying Issuers/ClusterIssuers, 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 namespace that the issuer resource was created in.", + "Name": "Issuer-Namespace" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The certificate reconcile ID that the controller used to issue this certificate.", + "Name": "Controller-Reconcile-Id" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The namespace that the CertificateSigningRequest resource was created in.", + "Name": "Certificate-Signing-Request-Namespace" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The namespace that the controller container is running in.", + "Name": "Controller-Namespace" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The type of issuer that the controller used to issue this certificate.", + "Name": "Controller-Kind" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The group name of the resource that the Issuer or ClusterIssuer controller is managing.", + "Name": "Controller-Resource-Group-Name" + }, + { + "AllowAPI": true, + "DataType": 1, + "Description": "The name of the K8s issuer resource", + "Name": "Issuer-Name" + } + ], + "ExpirationAlerts": [], + "IssuedCertAlerts": [], + "DeniedCertAlerts": [], + "PendingCertAlerts": [], + "Networks": [], + "WorkflowDefinitions": [], + "BuiltInReports": [], + "CustomReports": [], + "SecurityRoles": [] +} +EOF +kfutil import --metadata --file metadata.json +``` + +### Authentication +Authentication to the Command platform is done using basic authentication. The credentials must be provided as a Kubernetes `kubernetes.io/basic-auth` secret. These credentials should be for a user with "Certificate Enrollment: Enroll CSR" and "API: Read" permissions in Command. + +Create a `kubernetes.io/basic-auth` secret with the Keyfactor Command username and password: +```shell +cat < + password: +EOF +``` + +If the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root, the CA certificate must be provided as a Kubernetes secret. +```shell +kubectl -n command-issuer-system create secret generic command-ca-secret --from-file=ca.crt +``` + +### Creating Issuer and ClusterIssuer resources +The `command-issuer.keyfactor.com/v1alpha1` API version supports Issuer and ClusterIssuer resources. +The Command controller will automatically detect and process resources of both types. + +The Issuer resource is namespaced, while the ClusterIssuer resource is cluster-scoped. +For example, ClusterIssuer resources can be used to issue certificates for resources in multiple namespaces, whereas Issuer resources can only be used to issue certificates for resources in the same namespace. + +The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: +* `hostname` - The hostname of the Keyfactor Command server - The signer sets the protocol to `https` and automatically trims the trailing path from this field, if it exists. Additionally, the base Command API path is automatically set to `/KeyfactorAPI` and cannot be changed. +* `commandSecretName` - The name of the Kubernetes `kubernetes.io/basic-auth` secret containing credentials to the Keyfactor instance +* `certificateTemplate` - The short name corresponding to a template in Command that will be used to issue certificates. +* `certificateAuthorityLogicalName` - The logical name of the CA to use to sign the certificate request +* `certificateAuthorityHostname` - The CAs hostname to use to sign the certificate request +* `caSecretName` - The name of the Kubernetes secret containing the CA certificate. This field is optional and only required if the Command server is configured to use a self-signed certificate or with a certificate signed by an untrusted root. + +###### If a different combination of hostname/certificate authority/certificate profile/end entity profile is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. + +The following is an example of an Issuer resource: +```shell +cat <> command-issuer.yaml +apiVersion: command-issuer.keyfactor.com/v1alpha1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: issuer + app.kubernetes.io/instance: issuer-sample + app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-issuer +name: issuer-sample +spec: + hostname: "" + commandSecretName: "" + certificateTemplate: "" + certificateAuthorityLogicalName: "" + certificateAuthorityHostname: "" + caSecretName: "" +EOF +kubectl -n command-issuer-system apply -f command-issuer.yaml +``` + +###### :pushpin: Issuers can only issue certificates in the same namespace as the issuer resource. To issue certificates in multiple namespaces, use a ClusterIssuer. + +The following is an example of a ClusterIssuer resource: +```shell +cat <> command-clusterissuer.yaml +apiVersion: command-issuer.keyfactor.com/v1alpha1 +kind: ClusterIssuer +metadata: + labels: + app.kubernetes.io/name: clusterissuer + app.kubernetes.io/instance: clusterissuer-sample + app.kubernetes.io/part-of: command-issuer + app.kubernetes.io/created-by: command-issuer + name: clusterissuer-sample +spec: + hostname: "" + commandSecretName: "" + certificateTemplate: "" + certificateAuthorityLogicalName: "" + certificateAuthorityHostname: "" + caSecretName: "" +EOF +kubectl -n command-issuer-system apply -f command-clusterissuer.yaml +``` + +###### :pushpin: ClusterIssuers can issue certificates in any namespace. To issue certificates in a single namespace, use an Issuer. + +To create new resources from the above examples, replace the empty strings with the appropriate values and apply the resources to the cluster: +```shell +kubectl -n command-issuer-system apply -f issuer.yaml +kubectl -n command-issuer-system apply -f clusterissuer.yaml +``` + +### Using Issuer and ClusterIssuer resources +Once the Issuer and ClusterIssuer resources are created, they can be used to issue certificates using cert-manager. +The two most important concepts are `Certificate` and `CertificateRequest` resources. `Certificate` +resources represent a single X.509 certificate and its associated attributes, and automatically renews the certificate +and keeps it up to date. When `Certificate` resources are created, they create `CertificateRequest` resources, which +use an Issuer or ClusterIssuer to actually issue the certificate. + +###### To learn more about cert-manager, see the [cert-manager documentation](https://cert-manager.io/docs/). + +The following is an example of a Certificate resource. This resource will create a corresponding CertificateRequest resource, +and will use the `issuer-sample` Issuer resource to issue the certificate. Once issued, the certificate will be stored in a +Kubernetes secret named `command-certificate`. +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: command-certificate +spec: + commonName: command-issuer-sample + secretName: command-certificate + issuerRef: + name: issuer-sample + group: command-issuer.keyfactor.com + kind: Issuer +``` + +###### :pushpin: Certificate resources support many more fields than the above example. See the [Certificate resource documentation](https://cert-manager.io/docs/usage/certificate/) for more information. + +###### :pushpin: Since this certificate request called `command-certificate` is configured to use `issuer-sample`, it must be deployed in the same namespace as `issuer-sample`. + +Similarly, a CertificateRequest resource can be created directly. The following is an example of a CertificateRequest resource. +```yaml +apiVersion: cert-manager.io/v1 +kind: CertificateRequest +metadata: + name: command-certificate +spec: + request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2REQ0NBVndDQVFBd0x6RUxNQWtHQTFVRUN4TUNTVlF4SURBZUJnTlZCQU1NRjJWcVltTmhYM1JsY25KaApabTl5YlY5MFpYTjBZV05qTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4blNSCklqZDZSN2NYdUNWRHZscXlFcUhKalhIazljN21pNTdFY3A1RXVnblBXa0YwTHBVc25PMld6WTE1bjV2MHBTdXMKMnpYSURhS3NtZU9ZQzlNOWtyRjFvOGZBelEreHJJWk5SWmg0cUZXRmpyNFV3a0EySTdUb05veitET2lWZzJkUgo1cnNmaFdHMmwrOVNPT3VscUJFcWVEcVROaWxyNS85OVpaemlBTnlnL2RiQXJibWRQQ1o5OGhQLzU0NDZhci9NCjdSd2ludjVCMnNRcWM0VFZwTTh3Nm5uUHJaQXA3RG16SktZbzVOQ3JyTmw4elhIRGEzc3hIQncrTU9DQUw0T00KTkJuZHpHSm5KenVyS0c3RU5UT3FjRlZ6Z3liamZLMktyMXRLS3pyVW5keTF1bTlmTWtWMEZCQnZ0SGt1ZG0xdwpMUzRleW1CemVtakZXQi9yRVFJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUJhdFpIVTdOczg2Cmgxc1h0d0tsSi95MG1peG1vRWJhUTNRYXAzQXVFQ2x1U09mdjFDZXBQZjF1N2dydEp5ZGRha1NLeUlNMVNzazAKcWNER2NncUsxVVZDR21vRkp2REZEaEUxMkVnM0ZBQ056UytFNFBoSko1N0JBSkxWNGZaeEpZQ3JyRDUxWnk3NgpPd01ORGRYTEVib0w0T3oxV3k5ZHQ3bngyd3IwWTNZVjAyL2c0dlBwaDVzTHl0NVZOWVd6eXJTMzJYckJwUWhPCnhGMmNNUkVEMUlaRHhuMjR2ZEtINjMzSFo1QXd0YzRYamdYQ3N5VW5mVUE0ZjR1cHBEZWJWYmxlRFlyTW1iUlcKWW1NTzdLTjlPb0MyZ1lVVVpZUVltdHlKZTJkYXlZSHVyUUlpK0ZsUU5zZjhna1hYeG45V2drTnV4ZTY3U0x5dApVNHF4amE4OCs1ST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0t + issuerRef: + name: issuer-sample + group: command-issuer.keyfactor.com + kind: Issuer +``` + +### Approving Certificate Requests +Unless the cert-manager internal approver automatically approves the request, newly created CertificateRequest resources +will be in a `Pending` state until they are approved. CertificateRequest resources can be approved manually by using +[cmctl](https://cert-manager.io/docs/reference/cmctl/#approve-and-deny-certificaterequests). The following is an example +of approving a CertificateRequest resource named `command-certificate` in the `command-issuer-system` namespace. +```shell +cmctl -n command-issuer-system approve ejbca-certificate +``` + +Once a certificate request has been approved, the certificate will be issued and stored in the secret specified in the +CertificateRequest resource. The following is an example of retrieving the certificate from the secret. +```shell +kubectl get secret command-certificate -n command-issuer-system -o jsonpath='{.data.tls\.crt}' | base64 -d +``` + +###### To learn more about certificate approval and RBAC configuration, see the [cert-manager documentation](https://cert-manager.io/docs/concepts/certificaterequest/#approval). + +###### :pushpin: If the certificate was issued successfully, the Approved and Ready field will both be set to `True`. + +Next, see the [example usage](example.markdown) documentation for a complete example of using the Command Issuer for cert-manager. \ No newline at end of file diff --git a/docs/example.markdown b/docs/example.markdown new file mode 100644 index 0000000..63cacf5 --- /dev/null +++ b/docs/example.markdown @@ -0,0 +1,189 @@ + + Terraform logo + + +# Demo ClusterIssuer Usage with K8s Ingress + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +### Documentation Tree +* [Installation](install.markdown) +* [Usage](config_usage.markdown) +* [Customization](annotations.markdown) +* [Testing the Source](testing.markdown) + +This demo will show how to use a ClusterIssuer to issue a certificate for an Ingress resource. The demo uses the Kubernetes +`ingress-nginx` Ingress controller. If Minikube is being used, run the following command to enable the controller. +```shell +minikube addons enable ingress +kubectl get pods -n ingress-nginx +``` + +To manually deploy `ingress-nginx`, run the following command: +```shell +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml +``` + +Create a namespace for the demo: +```shell +kubectl create ns command-clusterissuer-demo +``` + +Deploy two Pods running the `hashicorp/http-echo` image: +```shell +cat < +``` + +Validate that the certificate was created: +```shell +kubectl -n command-clusterissuer-demo describe ingress command-ingress-demo +``` + +Test it out +```shell +curl -k https://localhost/apple +curl -k https://localhost/banana +``` + +Clean up +```shell +kubectl -n command-clusterissuer-demo delete ingress command-ingress-demo +kubectl -n command-clusterissuer-demo delete service apple-service banana-service +kubectl -n command-clusterissuer-demo delete pod apple-app banana-app +kubectl delete ns command-clusterissuer-demo +kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.0/deploy/static/provider/cloud/deploy.yaml +``` + +## Cleanup +To list the certificates and certificate requests created, run the following commands: +```shell +kubectl get certificates -n command-issuer-system +kubectl get certificaterequests -n command-issuer-system +``` + +To remove the certificate and certificate request resources, run the following commands: +```shell +kubectl delete certificate command-certificate -n command-issuer-system +kubectl delete certificaterequest command-certificate -n command-issuer-system +``` + +To list the issuer and cluster issuer resources created, run the following commands: +```shell +kubectl -n command-issuer-system get issuers.command-issuer.keyfactor.com +kubectl -n command-issuer-system get clusterissuers.command-issuer.keyfactor.com +``` + +To remove the issuer and cluster issuer resources, run the following commands: +```shell +kubectl -n command-issuer-system delete issuers.command-issuer.keyfactor.com +kubectl -n command-issuer-system delete clusterissuers.command-issuer.keyfactor.com +``` + +To remove the controller from the cluster, run: +```shell +make undeploy +``` + +To remove the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command, run: +```shell +make uninstall +``` \ No newline at end of file diff --git a/docs/install.markdown b/docs/install.markdown new file mode 100644 index 0000000..af1b0bc --- /dev/null +++ b/docs/install.markdown @@ -0,0 +1,125 @@ + + Terraform logo + + +# Installing the Keyfactor Command Issuer for cert-manager + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + +### Documentation Tree +* [Usage](config_usage.markdown) +* [Example Usage](example.markdown) +* [Customization](annotations.markdown) +* [Testing the Source](testing.markdown) + +### Requirements +* [Git](https://git-scm.com/) +* [Make](https://www.gnu.org/software/make/) +* [Docker](https://docs.docker.com/engine/install/) >= v20.10.0 +* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) >= v1.11.3 +* Kubernetes >= v1.19 + * [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), or [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) +* [Keyfactor Command](https://www.keyfactor.com/products/command/) >= v10.1.0 +* [cert-manager](https://cert-manager.io/docs/installation/) >= v1.11.0 +* [cmctl](https://cert-manager.io/docs/reference/cmctl/) + +Before starting, ensure that all of the above requirements are met, and that Keyfactor Command is properly configured according to the [product docs](https://software.keyfactor.com/Content/MasterTopics/Home.htm). Additionally, verify that at least one Kubernetes node is running by running the following command: + +```shell +kubectl get nodes +``` + +A static installation of cert-manager can be installed with the following command: + +```shell +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml +``` + +###### :pushpin: Running the static cert-manager configuration is not recommended for production use. For more information, see [Installing cert-manager](https://cert-manager.io/docs/installation/). + +### Building the Container Image + +The cert-manager external issuer for Keyfactor Command is distributed as source code, and the container must be built manually. The container image can be built using the following command: +```shell +make docker-build DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer +``` + +###### :pushpin: The container image can be built using Docker Buildx by running `make docker-buildx`. This will build the image for all supported platforms. + +To push the container image to a container registry, run the following command: +```shell +docker login +make docker-push DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer +``` + +### Installation from Manifests + +The cert-manager external issuer for Keyfactor Command can be installed using the manifests in the `config/` directory. + +1. Install the custom resource definitions (CRDs) for the cert-manager external issuer for Keyfactor Command: + + ```shell + make install + ``` + +2. Finally, deploy the controller to the cluster: + + ```shell + make deploy DOCKER_REGISTRY= DOCKER_IMAGE_NAME=keyfactor/command-cert-manager-issuer + ``` + +### Installation from Helm Chart + +The cert-manager external issuer for Keyfactor Command can also be installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). + +1. Add the Helm repository: + + ```bash + helm repo add command-issuer https://keyfactor.github.io/command-cert-manager-issuer + helm repo update + ``` + +2. Then, install the chart: + + ```bash + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/command-cert-manager-issuer \ + --set image.tag= + # --set image.pullPolicy=Never # Only required if using a local image + ``` + + a. Modifications can be made by overriding the default values in the `values.yaml` file with the `--set` flag. For example, to override the `replicaCount` value, run the following command: + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --create-namespace \ + --set image.repository=/keyfactor/command-cert-manager-issuer \ + --set image.tag= + --set replicaCount=2 + ``` + + b. Modifications can also be made by modifying the `values.yaml` file directly. For example, to override the + `replicaCount` value, modify the `replicaCount` value in the `values.yaml` file: + + ```yaml + cat < override.yaml + image: + repository: /keyfactor/command-cert-manager-issuer + pullPolicy: Never + tag: "latest" + replicaCount: 2 + EOF + ``` + + Then, use the `-f` flag to specify the `values.yaml` file: + + ```yaml + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + -f override.yaml + ``` + +Next, complete the [Usage](config_usage.markdown) steps to configure the cert-manager external issuer for Keyfactor Command. diff --git a/docs/testing.markdown b/docs/testing.markdown new file mode 100644 index 0000000..e633da2 --- /dev/null +++ b/docs/testing.markdown @@ -0,0 +1,32 @@ + + Terraform logo + + +# Testing the Controller Source Code + +[![Go Report Card](https://goreportcard.com/badge/github.com/Keyfactor/command-cert-manager-issuer)](https://goreportcard.com/report/github.com/Keyfactor/command-cert-manager-issuer) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://img.shields.io/badge/License-Apache%202.0-blue.svg) + + +### Documentation Tree +* [Installation](install.markdown) +* [Usage](config_usage.markdown) +* [Example Usage](example.markdown) +* [Customization](annotations.markdown) + +The test cases for the controller require a set of environment variables to be set. These variables are used to +authenticate to the Command server and to enroll a certificate. The test cases are run using the `make test` command. + +The following environment variables must be exported before testing the controller: +* `COMMAND_HOSTNAME` - The hostname of the Command server to use for testing. +* `COMMAND_USERNAME` - The username of an authorized Command user to use for testing. +* `COMMAND_PASSWORD` - The password of the authorized Command user to use for testing. +* `COMMAND_CERTIFICATE_TEMPLATE` - The name of the certificate template to use for testing. +* `COMMAND_CERTIFICATE_AUTHORITY_LOGICAL_NAME` - The logical name of the certificate authority to use for testing. +* `COMMAND_CERTIFICATE_AUTHORITY_HOSTNAME` - The hostname of the certificate authority to use for testing. +* `COMMAND_CA_CERT_PATH` - A relative or absolute path to the CA certificate that the Command server uses for TLS. The file must include the certificate in PEM format. + +To build the cert-manager external issuer for Keyfactor Command, run: +```shell +make test +``` \ No newline at end of file diff --git a/internal/controllers/certificaterequest_controller.go b/internal/controllers/certificaterequest_controller.go index a8cd5bf..65c531a 100644 --- a/internal/controllers/certificaterequest_controller.go +++ b/internal/controllers/certificaterequest_controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" "github.com/Keyfactor/command-issuer/internal/issuer/signer" issuerutil "github.com/Keyfactor/command-issuer/internal/issuer/util" cmutil "github.com/cert-manager/cert-manager/pkg/api/util" @@ -34,8 +35,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - - commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" ) var ( @@ -245,14 +244,15 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R meta.ControllerResourceGroupName = commandissuer.GroupVersion.Group meta.IssuerName = certificateRequest.Spec.IssuerRef.Name meta.IssuerNamespace = certificateRequest.Namespace - meta.ControllerReconcileId = fmt.Sprintf("%s", controller.ReconcileIDFromContext(ctx)) + meta.ControllerReconcileId = string(controller.ReconcileIDFromContext(ctx)) meta.CertificateSigningRequestNamespace = certificateRequest.Namespace - signed, err := commandSigner.Sign(ctx, certificateRequest.Spec.Request, meta) + leaf, chain, err := commandSigner.Sign(ctx, certificateRequest.Spec.Request, meta) if err != nil { return ctrl.Result{}, fmt.Errorf("%w: %v", errSignerSign, err) } - certificateRequest.Status.Certificate = signed + certificateRequest.Status.Certificate = leaf + certificateRequest.Status.CA = chain setReadyCondition(cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "Signed") return ctrl.Result{}, nil diff --git a/internal/controllers/certificaterequest_controller_test.go b/internal/controllers/certificaterequest_controller_test.go index da5d0e5..f113031 100644 --- a/internal/controllers/certificaterequest_controller_test.go +++ b/internal/controllers/certificaterequest_controller_test.go @@ -19,9 +19,6 @@ package controllers import ( "context" "errors" - "testing" - "time" - cmutil "github.com/cert-manager/cert-manager/pkg/api/util" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" @@ -40,22 +37,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "testing" commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" "github.com/Keyfactor/command-issuer/internal/issuer/signer" ) var ( - fixedClockStart = time.Date(2021, time.January, 1, 1, 0, 0, 0, time.UTC) - fixedClock = clock.RealClock{} + fixedClock = clock.RealClock{} ) type fakeSigner struct { errSign error } -func (o *fakeSigner) Sign(context.Context, []byte, signer.K8sMetadata) ([]byte, error) { - return []byte("fake signed certificate"), o.errSign +func (o *fakeSigner) Sign(context.Context, []byte, signer.K8sMetadata) ([]byte, []byte, error) { + return []byte("fake signed certificate"), []byte("fake ca chain"), o.errSign } func TestCertificateRequestReconcile(t *testing.T) { diff --git a/internal/issuer/signer/signer.go b/internal/issuer/signer/signer.go index a6c4884..187f55d 100644 --- a/internal/issuer/signer/signer.go +++ b/internal/issuer/signer/signer.go @@ -32,7 +32,8 @@ import ( const ( // Keyfactor enrollment PEM format - enrollmentPEMFormat = "PEM" + enrollmentPEMFormat = "PEM" + commandMetadataAnnotationPrefix = "metadata.command-issuer.keyfactor.com/" ) type K8sMetadata struct { @@ -51,6 +52,7 @@ type commandSigner struct { certificateAuthorityLogicalName string certificateAuthorityHostname string certManagerCertificateName string + customMetadata map[string]interface{} } type HealthChecker interface { @@ -61,7 +63,7 @@ type HealthCheckerBuilder func(context.Context, *commandissuer.IssuerSpec, map[s type CommandSignerBuilder func(context.Context, *commandissuer.IssuerSpec, map[string]string, map[string][]byte, map[string][]byte) (Signer, error) type Signer interface { - Sign(context.Context, []byte, K8sMetadata) ([]byte, error) + Sign(context.Context, []byte, K8sMetadata) ([]byte, []byte, error) } func CommandHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, authSecretData map[string][]byte, caSecretData map[string][]byte) (HealthChecker, error) { @@ -78,6 +80,10 @@ func CommandHealthCheckerFromIssuerAndSecretData(ctx context.Context, spec *comm } func CommandSignerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, annotations map[string]string, authSecretData map[string][]byte, caSecretData map[string][]byte) (Signer, error) { + return commandSignerFromIssuerAndSecretData(ctx, spec, annotations, authSecretData, caSecretData) +} + +func commandSignerFromIssuerAndSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, annotations map[string]string, authSecretData map[string][]byte, caSecretData map[string][]byte) (*commandSigner, error) { k8sLog := log.FromContext(ctx) signer := commandSigner{} @@ -119,15 +125,29 @@ func CommandSignerFromIssuerAndSecretData(ctx context.Context, spec *commandissu signer.certManagerCertificateName = value } - k8sLog.Info(fmt.Sprintf("Using certificate template \"%s\" and certificate authority \"%s\" (%s)", signer.certificateTemplate, signer.certificateAuthorityLogicalName, signer.certificateAuthorityHostname)) + k8sLog.Info(fmt.Sprintf("Using certificate template %q and certificate authority %q (%s)", signer.certificateTemplate, signer.certificateAuthorityLogicalName, signer.certificateAuthorityHostname)) + + signer.customMetadata = extractMetadataFromAnnotations(annotations) return &signer, nil } +func extractMetadataFromAnnotations(annotations map[string]string) map[string]interface{} { + metadata := make(map[string]interface{}) + + for key, value := range annotations { + if strings.HasPrefix(key, commandMetadataAnnotationPrefix) { + metadata[strings.TrimPrefix(key, commandMetadataAnnotationPrefix)] = value + } + } + + return metadata +} + func (s *commandSigner) Check() error { endpoints, _, err := s.client.StatusApi.StatusGetEndpoints(context.Background()).Execute() if err != nil { - detail := fmt.Sprintf("failed to get endpoints from Keyfactor Command") + detail := "failed to get endpoints from Keyfactor Command" var bodyError *keyfactor.GenericOpenAPIError ok := errors.As(err, &bodyError) @@ -149,17 +169,30 @@ func (s *commandSigner) Check() error { return errors.New("missing \"POST /Enrollment/CSR\" endpoint") } -func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMetadata) ([]byte, error) { +func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMetadata) ([]byte, []byte, error) { k8sLog := log.FromContext(ctx) csr, err := parseCSR(csrBytes) if err != nil { k8sLog.Error(err, "failed to parse CSR") - return nil, err + return nil, nil, err } // Log the common metadata of the CSR - k8sLog.Info(fmt.Sprintf("Found CSR wtih Common Name \"%s\" and %d DNS SANs, %d IP SANs, and %d URI SANs", csr.Subject.CommonName, len(csr.DNSNames), len(csr.IPAddresses), len(csr.URIs))) + k8sLog.Info(fmt.Sprintf("Found CSR wtih Common Name %q and %d DNS SANs, %d IP SANs, and %d URI SANs", csr.Subject.CommonName, len(csr.DNSNames), len(csr.IPAddresses), len(csr.URIs))) + + // Print the SANs + for _, dnsName := range csr.DNSNames { + k8sLog.Info(fmt.Sprintf("DNS SAN: %s", dnsName)) + } + + for _, ipAddress := range csr.IPAddresses { + k8sLog.Info(fmt.Sprintf("IP SAN: %s", ipAddress.String())) + } + + for _, uri := range csr.URIs { + k8sLog.Info(fmt.Sprintf("URI SAN: %s", uri.String())) + } modelRequest := keyfactor.ModelsEnrollmentCSREnrollmentRequest{ CSR: string(csrBytes), @@ -174,7 +207,12 @@ func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMe CommandMetaCertificateSigningRequestNamespace: k8sMeta.CertificateSigningRequestNamespace, }, Template: &s.certificateTemplate, - SANs: nil, // TODO figure out if the SANs from csr need to be copied here + SANs: nil, + } + + for metaName, value := range s.customMetadata { + k8sLog.Info(fmt.Sprintf("Adding metadata %q with value %q", metaName, value)) + modelRequest.Metadata[metaName] = value } var caBuilder strings.Builder @@ -189,7 +227,11 @@ func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMe commandCsrResponseObject, _, err := s.client.EnrollmentApi.EnrollmentPostCSREnroll(context.Background()).Request(modelRequest).XCertificateformat(enrollmentPEMFormat).Execute() if err != nil { - detail := fmt.Sprintf("error enrolling certificate with Command. verify that the certificate template \"%s\" exists and that the certificate authority \"%s\" (%s) is configured correctly", s.certificateTemplate, s.certificateAuthorityLogicalName, s.certificateAuthorityHostname) + detail := fmt.Sprintf("error enrolling certificate with Command. Verify that the certificate template %q exists and that the certificate authority %q (%s) is configured correctly.", s.certificateTemplate, s.certificateAuthorityLogicalName, s.certificateAuthorityHostname) + + if len(s.customMetadata) > 0 { + detail += " Also verify that the metadata fields provided exist in Command." + } var bodyError *keyfactor.GenericOpenAPIError ok := errors.As(err, &bodyError) @@ -199,15 +241,15 @@ func (s *commandSigner) Sign(ctx context.Context, csrBytes []byte, k8sMeta K8sMe k8sLog.Error(err, detail) - return nil, fmt.Errorf(detail) + return nil, nil, fmt.Errorf(detail) } certAndChain, err := getCertificatesFromCertificateInformation(commandCsrResponseObject.CertificateInformation) if err != nil { - return nil, err + return nil, nil, err } - k8sLog.Info(fmt.Sprintf("Successfully enrolled certificate with Command with subject \"%s\". Certificate has %d SANs", certAndChain[0].Subject, len(certAndChain[0].DNSNames)+len(certAndChain[0].IPAddresses)+len(certAndChain[0].URIs))) + k8sLog.Info(fmt.Sprintf("Successfully enrolled certificate with Command with subject %q. Certificate has %d SANs", certAndChain[0].Subject, len(certAndChain[0].DNSNames)+len(certAndChain[0].IPAddresses)+len(certAndChain[0].URIs))) // Return the certificate and chain in PEM format return compileCertificatesToPemBytes(certAndChain) @@ -235,20 +277,31 @@ func getCertificatesFromCertificateInformation(commandResp *keyfactor.ModelsPkcs // compileCertificatesToPemString takes a slice of x509 certificates and returns a string containing the certificates in PEM format // If an error occurred, the function logs the error and continues to parse the remaining objects. -func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, error) { - var pemBuilder strings.Builder - - for _, certificate := range certificates { - err := pem.Encode(&pemBuilder, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certificate.Raw, - }) - if err != nil { - return make([]byte, 0, 0), err +func compileCertificatesToPemBytes(certificates []*x509.Certificate) ([]byte, []byte, error) { + var leaf strings.Builder + var chain strings.Builder + + for i, certificate := range certificates { + if i == 0 { + err := pem.Encode(&leaf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + }) + if err != nil { + return make([]byte, 0), make([]byte, 0), err + } + } else { + err := pem.Encode(&chain, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + }) + if err != nil { + return make([]byte, 0), make([]byte, 0), err + } } } - return []byte(pemBuilder.String()), nil + return []byte(leaf.String()), []byte(chain.String()), nil } const ( @@ -261,19 +314,6 @@ const ( CommandMetaCertificateSigningRequestNamespace = "Certificate-Signing-Request-Namespace" ) -var ( - // Map used to determine if Keyfactor Command has a metadata field with a given name in O(1) time. - commandMetadataMap = map[string]string{ - CommandMetaControllerNamespace: "The namespace that the controller container is running in.", - CommandMetaControllerKind: "The type of issuer that the controller used to issue this certificate.", - CommandMetaControllerResourceGroupName: "The group name of the resource that the Issuer or ClusterIssuer controller is managing.", - CommandMetaIssuerName: "The name of the K8s issuer resource", - CommandMetaIssuerNamespace: "The namespace that the issuer resource was created in.", - CommandMetaControllerReconcileId: "The certificate reconcile ID that the controller used to issue this certificate.", - CommandMetaCertificateSigningRequestNamespace: "The namespace that the CertificateSigningRequest resource was created in.", - } -) - func createCommandClientFromSecretData(ctx context.Context, spec *commandissuer.IssuerSpec, authSecretData map[string][]byte, caSecretData map[string][]byte) (*keyfactor.APIClient, error) { k8sLogger := log.FromContext(ctx) @@ -308,7 +348,7 @@ func createCommandClientFromSecretData(ctx context.Context, spec *commandissuer. config.UserAgent = "command-issuer" // If the CA certificate is provided, add it to the EJBCA configuration - if caSecretData != nil && len(caSecretData) > 0 { + if len(caSecretData) > 0 { // There is no requirement that the CA certificate is stored under a specific key in the secret, so we can just iterate over the map var caCertBytes []byte for _, caCertBytes = range caSecretData { diff --git a/internal/issuer/signer/signer_test.go b/internal/issuer/signer/signer_test.go index b2a1eff..375ec9a 100644 --- a/internal/issuer/signer/signer_test.go +++ b/internal/issuer/signer/signer_test.go @@ -27,7 +27,11 @@ import ( "encoding/pem" "fmt" commandissuer "github.com/Keyfactor/command-issuer/api/v1alpha1" + "github.com/Keyfactor/keyfactor-go-client-sdk/api/keyfactor" + "github.com/stretchr/testify/assert" + "math/big" "os" + "reflect" "strings" "testing" "time" @@ -55,39 +59,344 @@ func TestCommandHealthCheckerFromIssuerAndSecretData(t *testing.T) { } func TestCommandSignerFromIssuerAndSecretData(t *testing.T) { - obj := testSigner{ - SignerBuilder: CommandSignerFromIssuerAndSecretData, + t.Run("ValidSigning", func(t *testing.T) { + obj := testSigner{ + SignerBuilder: CommandSignerFromIssuerAndSecretData, + } + + // Generate a test CSR to sign + csr, err := generateCSR("C=US,ST=California,L=San Francisco,O=Keyfactor,OU=Engineering,CN=example.com") + if err != nil { + t.Fatal(err) + } + + meta := K8sMetadata{ + ControllerNamespace: "test-namespace", + ControllerKind: "Issuer", + ControllerResourceGroupName: "test-issuer.example.com", + IssuerName: "test-issuer", + IssuerNamespace: "test-namespace", + ControllerReconcileId: "GUID", + CertificateSigningRequestNamespace: "test-namespace", + } + + start := time.Now() + signer, err := obj.SignerBuilder(getTestSignerConfigItems(t)) + if err != nil { + t.Fatal(err) + } + + leaf, chain, err := signer.Sign(context.Background(), csr, meta) + if err != nil { + t.Fatal(err) + } + t.Logf("Signing took %s", time.Since(start)) + + t.Logf("Signed certificate: %s", string(leaf)) + t.Logf("Chain: %s", string(chain)) + }) + + // Set up test data + + spec := commandissuer.IssuerSpec{ + Hostname: "example-hostname.com", + CertificateTemplate: "example-template", + CertificateAuthorityLogicalName: "example-logical-name", + CertificateAuthorityHostname: "ca-hostname.com", + SecretName: "example-secret-name", + CaSecretName: "example-ca-secret-name", + } + + authSecretData := map[string][]byte{ + "username": []byte("username"), + "password": []byte("password"), } - // Generate a test CSR to sign - csr, err := generateCSR("C=US,ST=California,L=San Francisco,O=Keyfactor,OU=Engineering,CN=example.com") + caSecretData := map[string][]byte{ + "tls.crt": []byte("ca-cert"), + } + + t.Run("MissingCertTemplate", func(t *testing.T) { + templateCopy := spec.CertificateTemplate + spec.CertificateTemplate = "" + // Create the signer + _, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) + if err == nil { + t.Errorf("expected error, got nil") + } + + spec.CertificateTemplate = templateCopy + }) + + t.Run("MissingCaLogicalName", func(t *testing.T) { + logicalNameCopy := spec.CertificateAuthorityLogicalName + spec.CertificateAuthorityLogicalName = "" + // Create the signer + _, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) + if err == nil { + t.Errorf("expected error, got nil") + } + + spec.CertificateAuthorityLogicalName = logicalNameCopy + }) + + t.Run("NoAnnotations", func(t *testing.T) { + // Create the signer + signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, make(map[string]string), authSecretData, caSecretData) + if err != nil { + t.Fatal(err) + } + + // If there are no annotations, the customMetadata map should be empty + if len(signer.customMetadata) != 0 { + t.Errorf("expected customMetadata to be empty, got %v", signer.customMetadata) + } + }) + + t.Run("MetadataAnnotations", func(t *testing.T) { + annotations := map[string]string{ + commandMetadataAnnotationPrefix + "key1": "value1", + commandMetadataAnnotationPrefix + "key2": "value2", + } + + // Create the signer + signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, annotations, authSecretData, caSecretData) + if err != nil { + t.Fatal(err) + } + + // If there are no annotations, the customMetadata map should be empty + if len(signer.customMetadata) != 2 { + t.Errorf("expected customMetadata to have 2 entries, got %v", signer.customMetadata) + } + + if value, ok := signer.customMetadata["key1"].(string); ok && value == "value1" { + // They are equal + } else { + t.Errorf("expected customMetadata key1 to be value1, got %v", signer.customMetadata["key1"]) + } + + if value, ok := signer.customMetadata["key2"].(string); ok && value == "value2" { + // They are equal + } else { + t.Errorf("expected customMetadata key1 to be value1, got %v", signer.customMetadata["key1"]) + } + }) + + t.Run("AnnotationDefaultOverrides", func(t *testing.T) { + annotations := map[string]string{ + "command-issuer.keyfactor.com/certificateTemplate": "TestCertificateTemplate", + "command-issuer.keyfactor.com/certificateAuthorityLogicalName": "TestCertificateAuthorityLogicalName", + "command-issuer.keyfactor.com/certificateAuthorityHostname": "TestCertificateAuthorityHostname", + "command-manager.io/certificate-name": "TestCertificateName", + } + + // Create the signer + signer, err := commandSignerFromIssuerAndSecretData(context.Background(), &spec, annotations, authSecretData, caSecretData) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, "TestCertificateTemplate", signer.certificateTemplate) + assert.Equal(t, "TestCertificateAuthorityLogicalName", signer.certificateAuthorityLogicalName) + assert.Equal(t, "TestCertificateAuthorityHostname", signer.certificateAuthorityHostname) + assert.Equal(t, "TestCertificateName", signer.certManagerCertificateName) + }) +} + +func TestCompileCertificatesToPemBytes(t *testing.T) { + // Generate two certificates for testing + cert1, err := generateSelfSignedCertificate() if err != nil { - t.Fatal(err) + t.Fatalf("failed to generate mock certificate: %v", err) + } + cert2, err := generateSelfSignedCertificate() + if err != nil { + t.Fatalf("failed to generate mock certificate: %v", err) } - meta := K8sMetadata{ - ControllerNamespace: "test-namespace", - ControllerKind: "Issuer", - ControllerResourceGroupName: "test-issuer.example.com", - IssuerName: "test-issuer", - IssuerNamespace: "test-namespace", - ControllerReconcileId: "GUID", - CertificateSigningRequestNamespace: "test-namespace", + tests := []struct { + name string + certificates []*x509.Certificate + expectedError bool + }{ + { + name: "No certificates", + certificates: []*x509.Certificate{}, + expectedError: false, + }, + { + name: "Single certificate", + certificates: []*x509.Certificate{cert1}, + expectedError: false, + }, + { + name: "Multiple certificates", + certificates: []*x509.Certificate{cert1, cert2}, + expectedError: false, + }, } - start := time.Now() - signer, err := obj.SignerBuilder(getTestSignerConfigItems(t)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err = compileCertificatesToPemBytes(tt.certificates) + if (err != nil) != tt.expectedError { + t.Errorf("expected error = %v, got %v", tt.expectedError, err) + } + }) + } +} + +func Test_extractMetadataFromAnnotations(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected map[string]interface{} + }{ + { + name: "empty annotations", + annotations: map[string]string{}, + expected: map[string]interface{}{}, + }, + { + name: "annotations without metadata prefix", + annotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + expected: map[string]interface{}{}, + }, + { + name: "annotations with metadata prefix", + annotations: map[string]string{ + commandMetadataAnnotationPrefix + "key1": "value1", + "key2": "value2", + }, + expected: map[string]interface{}{ + "key1": "value1", + }, + }, + { + name: "mixed annotations", + annotations: map[string]string{ + commandMetadataAnnotationPrefix + "key1": "value1", + commandMetadataAnnotationPrefix + "key2": "value2", + "key3": "value3", + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractMetadataFromAnnotations(tt.annotations) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func Test_createCommandClientFromSecretData(t *testing.T) { + cert1, err := generateSelfSignedCertificate() if err != nil { - t.Fatal(err) + t.Fatalf("failed to generate self-signed certificate: %v", err) } - signed, err := signer.Sign(context.Background(), csr, meta) + leafBytes, _, err := compileCertificatesToPemBytes([]*x509.Certificate{cert1}) if err != nil { - t.Fatal(err) + return } - t.Logf("Signing took %s", time.Since(start)) - t.Logf("Signed certificate: %s", string(signed)) + tests := []struct { + name string + spec commandissuer.IssuerSpec + authSecretData map[string][]byte + caSecretData map[string][]byte + verify func(*testing.T, *keyfactor.APIClient) error + expectedErr bool + }{ + { + name: "EmptySecretData", + authSecretData: map[string][]byte{ + "username": []byte(""), + "password": []byte(""), + }, + verify: func(t *testing.T, client *keyfactor.APIClient) error { + if client != nil { + return fmt.Errorf("expected client to be nil") + } + return nil + }, + expectedErr: true, + }, + { + name: "ValidAuthData", + spec: commandissuer.IssuerSpec{ + Hostname: "hostname", + }, + authSecretData: map[string][]byte{ + "username": []byte("username"), + "password": []byte("password"), + }, + verify: func(t *testing.T, client *keyfactor.APIClient) error { + if client == nil { + return fmt.Errorf("expected client to be non-nil") + } + + if client.GetConfig().Host != "hostname" { + return fmt.Errorf("expected hostname to be hostname, got %s", client.GetConfig().Host) + } + + if client.GetConfig().BasicAuth.UserName != "username" { + return fmt.Errorf("expected username to be username, got %s", client.GetConfig().BasicAuth.UserName) + } + + if client.GetConfig().BasicAuth.Password != "password" { + return fmt.Errorf("expected password to be password, got %s", client.GetConfig().BasicAuth.Password) + } + + return nil + }, + expectedErr: false, + }, + { + name: "InvalidCaData", + spec: commandissuer.IssuerSpec{ + Hostname: "hostname", + }, + authSecretData: map[string][]byte{ + "username": []byte("username"), + "password": []byte("password"), + }, + caSecretData: map[string][]byte{ + "tls.crt": leafBytes, + }, + verify: func(t *testing.T, client *keyfactor.APIClient) error { + if client == nil { + return fmt.Errorf("expected client to be non-nil") + } + + return nil + }, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := createCommandClientFromSecretData(context.Background(), &tt.spec, tt.authSecretData, tt.caSecretData) + if (err != nil) != tt.expectedErr { + t.Errorf("expected error = %v, got %v", tt.expectedErr, err) + } + if err = tt.verify(t, result); err != nil { + t.Error(err) + } + }) + } } func getTestHealthCheckerConfigItems(t *testing.T) (context.Context, *commandissuer.IssuerSpec, map[string][]byte, map[string][]byte) { @@ -142,7 +451,7 @@ func getTestSignerConfigItems(t *testing.T) (context.Context, *commandissuer.Iss // Read the CA cert from the file system. caCertBytes, err := os.ReadFile(pathToCaCert) if err != nil { - t.Log("CA cert not found, assuming that EJBCA is using a trusted CA") + t.Log("CA cert not found, assuming that Command is using a trusted CA") } caSecretData := map[string][]byte{} @@ -158,18 +467,19 @@ func generateCSR(subject string) ([]byte, error) { subj, err := parseSubjectDN(subject, false) if err != nil { - return make([]byte, 0, 0), err + return make([]byte, 0), err } template := x509.CertificateRequest{ Subject: subj, SignatureAlgorithm: x509.SHA256WithRSA, + DNSNames: []string{subj.CommonName}, } var csrBuf bytes.Buffer csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, keyBytes) err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) if err != nil { - return make([]byte, 0, 0), err + return make([]byte, 0), err } return csrBuf.Bytes(), nil @@ -208,7 +518,7 @@ func parseSubjectDN(subject string, randomizeCn bool) (pkix.Name, error) { name.OrganizationalUnit = []string{value} case "CN": if randomizeCn { - value = fmt.Sprintf("%s-%s", value, generateRandomString(5)) + name.CommonName = fmt.Sprintf("%s-%s", value, generateRandomString(5)) } else { name.CommonName = value } @@ -219,3 +529,31 @@ func parseSubjectDN(subject string, randomizeCn bool) (pkix.Name, error) { return name, nil } + +func generateSelfSignedCertificate() (*x509.Certificate, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + return nil, err + } + + return cert, nil +}