diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 61f699d..fa2925b 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -1,20 +1,12 @@ name: Build and Release Helm Chart on: - pull_request: - branches: - - 'release-*' - 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 - + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' # 1.2.3 (exact match) - release candidates are excluded + jobs: helm: runs-on: ubuntu-latest - if: github.event.pull_request.merged == true steps: - name: Set IMAGE_NAME run: | @@ -23,20 +15,20 @@ jobs: # Checkout code # https://github.com/actions/checkout - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@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 + uses: docker/metadata-action@v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Set version from DOCKER_METADATA_OUTPUT_VERSION as environment variable - name: Set Version run: | - echo "VERSION=${DOCKER_METADATA_OUTPUT_VERSION:8}.0" >> $GITHUB_ENV # Eventually will build this into Keyfactor bootstrap + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV # Eventually will build this into Keyfactor bootstrap # Change version and appVersion in Chart.yaml to the tag in the closed PR - name: Update Helm App/Chart Version @@ -48,7 +40,7 @@ jobs: # Setup Helm # https://github.com/Azure/setup-helm - name: Install Helm - uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 + uses: azure/setup-helm@v3.5 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -61,7 +53,7 @@ jobs: # Build and release Helm chart to GitHub Pages # https://github.com/helm/chart-releaser-action - name: Run chart-releaser - uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0 + uses: helm/chart-releaser-action@v1.5.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" with: diff --git a/.github/workflows/keyfactor-bootstrap-workflow.yml b/.github/workflows/keyfactor-bootstrap-workflow.yml index c24a986..e4f9300 100644 --- a/.github/workflows/keyfactor-bootstrap-workflow.yml +++ b/.github/workflows/keyfactor-bootstrap-workflow.yml @@ -14,10 +14,10 @@ jobs: build: name: Build and Lint runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 8 steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4.2.1 with: go-version-file: 'go.mod' cache: true @@ -35,9 +35,9 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@v4 - name: Set up Go 1.x - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@v4.2.1 with: go-version-file: 'go.mod' cache: true @@ -46,7 +46,7 @@ jobs: run: go test -v ./... call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v3 + uses: keyfactor/actions/.github/workflows/starter.yml@3.2.0 needs: test secrets: token: ${{ secrets.V2BUILDTOKEN}} diff --git a/.gitignore b/.gitignore index ef9e7e2..79cba22 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,8 @@ bin # Helm *.tgz -.DS_Store \ No newline at end of file +.DS_Store + +**/.env +**/.env.* +!**/.env.example \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 268ebff..d58c875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,40 @@ -# v1.0.4 - +# v2.2.0 ## 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. +- Added support for enrolling CSRs with [Enrollment Patterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm), a new feature introduced in Keyfactor Command 25.1. [Release notes](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReleaseNotes/Release2511.htm) + - Usage of `CertificateTemplate` is still supported, but if using Keyfactor Command 25.1 and above, it is recommended to start using Enrollment Patterns in your issuer specification. You may use `EnrollmentPatternId` or `EnrollmentPatternName` in your specification. +- When using ambient credentials, some relevant token claims (subject, issuer, object ID, etc.) are logged for easier debugging and setup for security roles and identity providers. + +## Chores +- Updated documentation for using ambient credentials with Azure Kuberentes Services. +- Removed documentation for using ambient credentials with Google Kubernetes Engine. As of writing, Google is not a supported identity provider in Keyfactor Command. +- Migrated from using [keyfactor-go-client](https://github.com/Keyfactor/keyfactor-go-client) to [keyfactor-go-client-sdk](https://github.com/keyfactor/keyfactor-go-client-sdk). ## 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. +- Fix the Helm chart releaser job to not run into issues with overlapping Helm chart versions. -# v1.0.5 +# v2.1.1 -## Features -* feat(controller): Implement Kubernetes `client-go` REST client for Secret/ConfigMap retrieval to bypass `controller-runtime` caching system. This enables the reconciler to retrieve Secret and ConfigMap resources at the namespace scope with only namespace-level permissions. +## Fixes +- Update Helm chart deployment template to resolve Docker image metadata issue. + +## Chores +- Update documentation for more clear instructions on deploying workloads to Azure Kubernetes Service and Google Kubernetes Engine, as well as permissions needed on Command Security Roles. + +# v2.1.0 ## Fixes -* fix(helm): Add configuration flag to configure chart to either grant cluster-scoped or namespace-scoped access to Secret and ConfigMap API -* fix(controller): Add logic to read secret from reconciler namespace or Issuer namespace depending on Helm configuration. +- Updated library golang.org/x/crypto to version v0.33.0 to address authorization bypass vulnerability (https://github.com/advisories/GHSA-v778-237x-gjrc) +- Bug fix for Google ambient credentials + +# v2.0.2 + +## Fixes +- Bug fix in Helm chart release action + +# v2.0.1 + +## Fixes +- Change Helm release trigger from `v*` to `release-*` to support Keyfactor Bootstrap Workflow # v2.0.0 @@ -33,26 +50,23 @@ - Refactor unit tests to use fake Command API instead of requiring live Command server. - Write e2e integration test. -# v2.0.1 - -## Fixes -- Change Helm release trigger from `v*` to `release-*` to support Keyfactor Bootstrap Workflow +# v1.0.5 -# v2.0.2 +## Features +* feat(controller): Implement Kubernetes `client-go` REST client for Secret/ConfigMap retrieval to bypass `controller-runtime` caching system. This enables the reconciler to retrieve Secret and ConfigMap resources at the namespace scope with only namespace-level permissions. ## Fixes -- Bug fix in Helm chart release action - -# v2.1.0 +* fix(helm): Add configuration flag to configure chart to either grant cluster-scoped or namespace-scoped access to Secret and ConfigMap API +* fix(controller): Add logic to read secret from reconciler namespace or Issuer namespace depending on Helm configuration. -## Fixes -- Updated library golang.org/x/crypto to version v0.33.0 to address authorization bypass vulnerability (https://github.com/advisories/GHSA-v778-237x-gjrc) -- Bug fix for Google ambient credentials +# v1.0.4 -# v2.1.1 +## 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 -- Update Helm chart deployment template to resolve Docker image metadata issue. - -## Chores -- Update documentation for more clear instructions on deploying workloads to Azure Kubernetes Service and Google Kubernetes Engine, as well as permissions needed on Command Security Roles. +* 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. diff --git a/Dockerfile b/Dockerfile index cce20f7..cb1b130 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.23.4 AS builder +FROM golang:1.24 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/README.md b/README.md index 3dc8c97..39df70a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Before continuing, ensure that the following requirements are met: - `/Status/Endpoints` - `/Enrollment/CSR` - `/MetadataFields` + - `/EnrollmentPatterns` (Keyfactor Command 25.1 and above) - Kubernetes >= v1.19 - [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/), etc. > You must have permission to create [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in your Kubernetes cluster. @@ -55,7 +56,7 @@ Before continuing, ensure that the following requirements are met: ## Configuring Command -Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template suitable for your usecase. Additionally, you should ensure that the identity used by the Issuer/ClusterIssuer has the appropriate permissions in Command. +Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template / Enrollment Pattern suitable for your use case. Additionally, you should ensure that the [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) used by the Issuer/ClusterIssuer has the appropriate permissions in Command. 1. **Create or identify a Certificate Authority** @@ -65,40 +66,50 @@ Command Issuer enrolls certificates by submitting a POST request to the Command The CA that you choose must be configured to allow CSR Enrollment. -2. **Identify a Certificate Template** +2. **Identify a Certificate Template / Enrollment Pattern** + + Keyfactor Command 25.1 introduces support for [Enrollment Patterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReleaseNotes/Release2511.htm#Highlights), which allow an easy way to share certificate configuration without requiring multiple certificate templates. Certificate Template configuration has been moved to the Enrollment Patterns screen. Certificate Templates will still be supported in Issuer / ClusterIssuer configuration, but it is recommended to start using Enrollment Patterns for Keyfactor Command versions 25.1 and above. + + - If you don't have any suitable Enrollment Patterns, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) or reach out to your Keyfactor support representative to learn more. Certificate Templates in Command define properties and constraints of the certificates being issued. This includes settings like key usage, extended key usage, validity period, allowed key algorithms, and signature algorithms. They also control the type of information that end entities must provide and how that information is validated before issuing certificates. - If you don't have any suitable Certificate Templates, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm?Highlight=Certificate%20Template) or reach out to your Keyfactor support representative to learn more. - The Certificate Template that you choose must be configured to allow CSR Enrollment. + The Certificate Template / Enrollment Pattern that you choose must be configured to allow CSR Enrollment. - You should make careful note of the allowed Key Types and Key Sizes on the Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the key `algorithm` and `size` are allowed by your Certificate Template in Command. + You should make careful note of the allowed Key Types and Key Sizes on the Certificate Template / Enrollment Pattern. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the key `algorithm` and `size` are allowed by your Certificate Template / Enrollment Pattern in Command. - The same goes for **Enrollment RegExes** and **Policies** defined on your Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the `subject`, `commonName`, `dnsNames`, etc. are allowed and/or configured correctly by your Certificate Template in Command. + The same goes for **Enrollment RegExes** and **Policies** defined on your Certificate Template / Enrollment Pattern. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the `subject`, `commonName`, `dnsNames`, etc. are allowed and/or configured correctly by your Certificate Template / Enrollment Pattern in Command. 3. **Configure Command Security Roles and Claims** In Command, Security Roles define groups of users or administrators with specific permissions. Users and subjects are identified by Claims. By adding a Claim to a Security Role, you can define what actions the user or subject can perform and what parts of the system it can interact with. - The security role will need to be added as an Allowed Requester Security Role on the Certificate Authority and Certificate Template configured in the previous two steps. + The security role will need to be added as an **Allowed Requester Security Role** on the Certificate Authority and Certificate Template / Enrollment Pattern configured in the previous two steps. - If you haven't created Roles and Access rules before, [this guide](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) provides a primer on these concepts in Command. If your security policy requires fine-grain access control, Command Issuer requires the following Access Rules: - | Global Permissions | Permission Model (Version Two) | Permission Model (Version One) | - |-----------------------------------------|---|---| - | Metadata > Types > Read | `/metadata/types/read/` | `CertificateMetadataTypes:Read` | - | Certificates > Enrollment > Csr | `/certificates/enrollment/csr/` | `CertificateEnrollment:EnrollCSR` | + | Global Permissions | Permission Model (Version Two) | Permission Model (Version One) | Notes + |-----------------------------------------|---|---|--| + | Metadata > Types > Read | `/metadata/types/read/` | `CertificateMetadataTypes:Read` | | + | Certificates > Enrollment > Csr | `/certificates/enrollment/csr/` | `CertificateEnrollment:EnrollCSR` | | + | Enrollment Patterns > Read (Optional) | `/enrollment_pattern/read/` | N/A | Required if using `EnrollmentPatternName` | > Documentation for [Version Two Permission Model](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityRolePermissions.htm#VersionTwoPermissionModel) and [Version One Permission Model](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityRolePermissions.htm#VersionOnePermissionModel) ![Permission Metadata Read](./docsource/images/security_permission_metadata_read.png) + ![Permission Certificate CSR Enrollment](./docsource/images/security_permission_enrollment_csr.png) + ![Certificate Authority Allowed Requester](./docsource/images/ca_allowed_requester.png) + ![Certificate Template Allowed Requester](./docsource/images/cert_template_allowed_requester.png) +![Enrollment Pattern Allowed Requester](./docsource/images/enrollment_pattern_allowed_requester.png) + ## Installing Command Issuer Command Issuer is installed using a Helm chart. The chart is available in the [Command cert-manager Helm repository](https://keyfactor.github.io/command-cert-manager-issuer/). @@ -124,22 +135,37 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C --create-namespace ``` + Optionally, set the Docker image tag of command-cert-manager-issuer to deploy ([available tags](https://hub.docker.com/r/keyfactor/command-cert-manager-issuer/tags)) + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --set "image.tag=latest" \ + --create-namespace + ``` + > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. # Authentication -Command Issuer supports authentication to Command using one of the following methods: +## Explicit Credentials -- Basic Authentication (username and password) -- OAuth 2.0 "client credentials" token flow (sometimes called two-legged OAuth 2.0) +Command Issuer supports explicit credentials authentication to Command using one of the following methods: + +- [Basic Authentication](#basic-auth) (username and password) +- [OAuth 2.0 "client credentials" token flow](#oauth) (sometimes called two-legged OAuth 2.0) These credentials must be configured using a Kubernetes Secret. By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). > Command Issuer can read secrets in the Issuer namespace if `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag is set when installing the Helm chart. +## Ambient Credentials + Command Issuer also supports ambient authentication, where a token is fetched from an Authorization Server using a cloud provider's auth infrastructure and passed to Command directly. The following methods are supported: -- Managed Identity Using Azure Entra ID Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) +- [Managed Identity Using Azure Entra ID Workload Identity](./docs/ambient-providers/azure.md) (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) + +If you are running your Kubernetes workload in a cloud provider not listed above, you can use workload identity federation with [Azure AD](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation). ## Basic Auth @@ -180,157 +206,7 @@ kubectl -n command-issuer-system create secret generic command-secret \ ## Managed Identity Using Azure Entra ID Workload Identity (AKS) -Azure Entra ID workload identity in Azure Kubernetes Service (AKS) allows Command Issuer to exchange a Kubernetes ServiceAccount Token for an Azure Entra ID access token, which is then used to authenticate to Command. - -At this time, Azure Kuberentes Services workload identity federation is best supported by [User Assigned Managed Identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-manage-user-assigned-managed-identities?pivots=identity-mi-methods-azp). Other identity solutions such as Azure AD Service Principals are not supported. - -Here is a guide on how to use Azure User Assigned Managed Identities to authenticate your AKS workload with your Keyfactor Command instance. - -1. Reconfigure the AKS cluster to enable workload identity federation. - - ```shell - export CLUSTER_NAME= - export RESOURCE_GROUP= - az aks update \ - --name ${CLUSTER_NAME} \ - --resource-group ${RESOURCE_GROUP} \ - --enable-oidc-issuer \ - --enable-workload-identity - ``` - - > The [Azure Workload Identity extension can be installed on non-AKS or self-managed clusters](https://azure.github.io/azure-workload-identity/docs/installation.html) if you're not using AKS. - > - > Refer to the [AKS documentation](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster) for more information on the `--enable-workload-identity` feature. - -2. Create a User Assigned Managed Identity in Azure. - - ```shell - export IDENTITY_NAME=command-issuer - az identity create --name "${IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP}" - ``` - > Read more about [the `az identity` command](https://learn.microsoft.com/en-us/cli/azure/identity?view=azure-cli-latest). - -3. Reconfigure or deploy Command Issuer with extra labels for the Azure Workload Identity webhook, which will result in the Command Issuer controller Pod having an extra volume containing a Kubernetes ServiceAccount token which it will exchange for a token from Azure. - - ```shell - export UAMI_CLIENT_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query clientId --output tsv) - - echo "Identity Client ID: ${UAMI_CLIENT_ID}" - - helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ - --namespace command-issuer-system \ - --create-namespace \ - --set "fullnameOverride=command-cert-manager-issuer" \ - --set-string "podLabels.azure\.workload\.identity/use=true" \ - --set-string "serviceAccount.labels.azure\.workload\.identity/use=true" \ - --set-string "serviceAccount.annotations.azure\.workload\.identity/client-id=${UAMI_CLIENT_ID}" - ``` - - If successful, the Command Issuer Pod will have new environment variables and the Azure WI ServiceAccount token as a projected volume: - - ```shell - kubectl -n command-issuer-system describe pod - ``` - - ```shell - Containers: - command-cert-manager-issuer: - ... - Environment: - AZURE_CLIENT_ID: - AZURE_TENANT_ID: - AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token - AZURE_AUTHORITY_HOST: https://login.microsoftonline.com/ - Mounts: - /var/run/secrets/azure/tokens from azure-identity-token (ro) - /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6rmzz (ro) - ... - Volumes: - ... - azure-identity-token: - Type: Projected (a volume that contains injected data from multiple sources) - TokenExpirationSeconds: 3600 - ``` - - > Refer to [Azure Workload Identity docs](https://azure.github.io/azure-workload-identity/docs/installation/mutating-admission-webhook.html) more information on the role of the Mutating Admission Webhook. - -4. Associate a Federated Identity Credential (FIC) with the User Assigned Managed Identity. The FIC allows Command Issuer to act on behalf of the Managed Identity by telling Azure to expect: - - The `iss` claim of the ServiceAccount token to match the cluster's OIDC Issuer. Azure will also use the Issuer URL to download the JWT signing certificate. - - The `sub` claim of the ServiceAccount token to match the ServiceAccount's name and namespace. - - ```shell - export SERVICE_ACCOUNT_NAME=command-cert-manager-issuer # This is the default Kubernetes ServiceAccount used by the Command Issuer controller. - export SERVICE_ACCOUNT_NAMESPACE=command-issuer-system # This is the default namespace for Command Issuer used in this doc. - - export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --query "oidcIssuerProfile.issuerUrl" -o tsv) - az identity federated-credential create \ - --name "${IDENTITY_NAME}-federated-credentials" \ - --identity-name "${IDENTITY_NAME}" \ - --resource-group "${RESOURCE_GROUP}" \ - --issuer "${SERVICE_ACCOUNT_ISSUER}" \ - --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" \ - --audiences "api://AzureADTokenExchange" - ``` - - > Read more about [Workload Identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) in the Entra ID documentation. - > - > Read more about [the `az identity federated-credential` command](https://learn.microsoft.com/en-us/cli/azure/identity/federated-credential?view=azure-cli-latest). - -5. Get the Managed Identity's Principal ID and Entra Identity Provider Information - - ```shell - export UAMI_PRINCIPAL_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query principalId --output tsv) - export CURRENT_TENANT=$(az account show --query tenantId --output tsv) - echo "UAMI Principal ID: ${UAMI_PRINCIPAL_ID}" - - echo "View then OIDC configuration for the Entra OIDC token issuer: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0/.well-known/openid-configuration" - - echo "Authority: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0" - ``` - - > **IMPORTANT NOTE**: The Microsoft Entra Identity Provider is associated with your Azure tenant ID. Multi-tenant Azure workloads will require a Command Identity Provider for each tenant. - -6. Add the Microsoft Entra ID as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider) using the identity provider information from the previous step, and [add the Managed Identity's Principal ID as an `OAuth Subject` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. - -## Google Kubernetes Engine (GKE) Workload Identity - -Google Kuberentes Engine (GKE) supports the ability to authenticate your GKE workloads using workload identity. - -By default, GKE clusters are assigned the [default service account](https://cloud.google.com/compute/docs/access/service-accounts#token) for your Google project. This service account is used to generate an ID token for your workload. However, you may opt to use [Workload Identity Federation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#metadata-server) to your GKE cluster. - -1. Get the OAuth Client and Identity Provider for your GKE Cluster - - Regardless if you are using the default service account or a custom service account, the following script will help you derive your GKE cluster's OAuth Client: - - ```shell - export CLUSTER_NAME= - export GCLOUD_REGION= - export GCLOUD_PROJECT_ID=$(gcloud config get-value project) # populate with the current PROJECT_ID context - export GCLOUD_PROJECT_NUMBER=$(gcloud projects describe $GCLOUD_PROJECT_ID --format="value(projectNumber)") - - export GCLOUD_SERVICE_ACCOUNT=$(gcloud container clusters describe $CLUSTER_NAME \ - --zone $GCLOUD_REGION \ - --format="value(nodeConfig.serviceAccount)") - - if [[ "$GCLOUD_SERVICE_ACCOUNT" == "default" ]]; then - # Override service account with default compute service account - GCLOUD_SERVICE_ACCOUNT="$GCLOUD_PROJECT_NUMBER-compute@developer.gserviceaccount.com" - fi - - echo "Service account: $GCLOUD_SERVICE_ACCOUNT" - - # Get OAuth2 Client ID of service account - export GCLOUD_SERVICE_ACCOUNT_CLIENT_ID=$(gcloud iam service-accounts describe $GCLOUD_SERVICE_ACCOUNT \ - --format="value(oauth2ClientId)") - - echo "Service account OAuth2 client ID: $GCLOUD_SERVICE_ACCOUNT_CLIENT_ID" - - echo "View the OIDC configuration for Google's OIDC token issuer: https://accounts.google.com/.well-known/openid-configuration" - - echo "Authority: https://accounts.google.com" - ``` - -2. Add Google as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider) using the identity provider information from the previous step, and [add the Service Account's OAuth Client ID as an `OAuth Subject` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. +This section has moved. Please refer to [this link](./docs/ambient-providers/azure.md) for documentation on configuring ambient credentials with AKS. # CA Bundle @@ -353,6 +229,8 @@ For example, ClusterIssuer resources can be used to issue certificates for resou export COMMAND_CA_HOSTNAME="" # Only required for non-HTTPS CA types export COMMAND_CA_LOGICAL_NAME="" export CERTIFICATE_TEMPLATE_SHORT_NAME="" + export ENROLLMENT_PATTERN_NAME="" + export ENROLLMENT_PATTERN_ID="" ``` The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: @@ -360,12 +238,14 @@ For example, ClusterIssuer resources can be used to issue certificates for resou |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | hostname | The hostname of the Command API Server. | | apiPath | (optional) The base path of the Command REST API. Defaults to `KeyfactorAPI`. | - | commandSecretName | The name of the Kubernetes secret containing basic auth credentials or OAuth 2.0 credentials | + | commandSecretName | (optional) The name of the Kubernetes secret containing basic auth credentials or OAuth 2.0 credentials. Omit if using ambient credentials. | | caSecretName | (optional) The name of the Kubernetes secret containing the CA certificate. Required if the Command API uses a self-signed certificate or it was signed by a CA that is not widely trusted. | | certificateAuthorityLogicalName | The logical name of the Certificate Authority to use in Command. For example, `Sub-CA` | | certificateAuthorityHostname | (optional) The hostname of the Certificate Authority specified by `certificateAuthorityLogicalName`. This field is usually only required if the CA in Command is a DCOM (MSCA-like) CA. | - | certificateTemplate | The Short Name of the Certificate Template to use when this Issuer/ClusterIssuer enrolls CSRs. | - | scopes | (Optional) If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | + | enrollmentPatternId | The ID of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternId` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | + | enrollmentPatternName | The Name of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternName` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. If using `enrollmentPatternName`, your security role must have `/enrollment_pattern/read/` permission. | + | certificateTemplate | The Short Name of the Certificate Template to use when this Issuer/ClusterIssuer enrolls CSRs. **Deprecated in favor of [Enrollment Patterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/Enrollment-Patterns.htm) as of Keyfactor Command 25.1**. If `certificateTemplate` and either `enrollmentPatternName` or `enrollmentPatternId` are specified, the enrollment pattern parameter will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | + | scopes | (Optional) Required if using ambient credentials with Azure AKS. If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | | audience | (Optional) If using ambient credentials, this audience will be put on the access token generated by the ambient credentials' token provider, if applicable. Google's ambient credential token provider generates an OIDC ID Token. If this value is not provided, it will default to `command`. | > If a different combination of hostname/certificate authority/certificate template is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. @@ -386,13 +266,15 @@ For example, ClusterIssuer resources can be used to issue certificates for resou spec: hostname: "$HOSTNAME" apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically - commandSecretName: "command-secret" # references the secret created above + commandSecretName: "command-secret" # references the secret created above. Omit if using ambient credentials. caSecretName: "command-ca-secret" # references the secret created above # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" - # scopes: "openid email https://example.com/.default" # Uncomment if desired + enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. + # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired EOF @@ -412,13 +294,15 @@ For example, ClusterIssuer resources can be used to issue certificates for resou spec: hostname: "$HOSTNAME" apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically - commandSecretName: "command-secret" # references the secret created above + commandSecretName: "command-secret" # references the secret created above. Omit if using ambient credentials. caSecretName: "command-ca-secret" # references the secret created above # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" - # scopes: "openid email https://example.com/.default" # Uncomment if desired + enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. + # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired EOF @@ -466,7 +350,7 @@ spec: request: ``` -> All fields in Command Issuer and ClusterIssuer `spec` can be overridden by applying Kubernetes Annotations to Certificates _and_ CertificateRequests. See [runtime customization for more](docs/annotations.md) +> All fields in Command Issuer and ClusterIssuer `spec` can be overridden by applying Kubernetes Annotations to Certificates _and_ CertificateRequests. See [runtime customization for more](#overriding-the-issuerclusterissuer-spec-using-kubernetes-annotations-on-certificaterequest-resources) ## Approving Certificate Requests @@ -488,11 +372,13 @@ kubectl get secret command-certificate -o jsonpath='{.data.tls\.crt}' | base64 - ## Overriding the Issuer/ClusterIssuer `spec` using Kubernetes Annotations on CertificateRequest Resources -Command Issuer allows you to override the `certificateAuthorityHostname`, `certificateAuthorityLogicalName`, and `certificateTemplate` by setting Kubernetes Annotations on CertificateRequest resources. This may be useful if certain enrollment scenarios require a different Certificate Authority or Certificate Template, but you don't want to create a new Issuer/ClusterIssuer. +Command Issuer allows you to override the `certificateAuthorityHostname`, `certificateAuthorityLogicalName`, `certificateTemplate`, `enrollmentPatternName`, and `enrollmentPatternId` by setting Kubernetes Annotations on CertificateRequest resources. This may be useful if certain enrollment scenarios require a different Certificate Authority or Certificate Template, but you don't want to create a new Issuer/ClusterIssuer. - `command-issuer.keyfactor.com/certificateAuthorityHostname` overrides `certificateAuthorityHostname` - `command-issuer.keyfactor.com/certificateAuthorityLogicalName` overrides `certificateAuthorityLogicalName` - `command-issuer.keyfactor.com/certificateTemplate` overrides `certificateTemplate` +- `command-issuer.keyfactor.com/enrollmentPatternName` overrides `enrollmentPatternName` +- `command-issuer.keyfactor.com/enrollmentPatternId` overrides `enrollmentPatternId`. Needs to be in string format. > cert-manager copies Annotations set on Certificate resources to the corresponding CertificateRequest. @@ -506,6 +392,8 @@ Command Issuer allows you to override the `certificateAuthorityHostname`, `certi > kind: Certificate > metadata: > annotations: +> command-issuer.keyfactor.com/enrollmentPatternId: "1234" +> command-issuer.keyfactor.com/enrollmentPatternName: "Kubernetes Enrollment Pattern" > command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" > command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" > metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" @@ -543,6 +431,30 @@ Keyfactor Command allows users to [attach custom metadata to certificates](https metadata.command-issuer.keyfactor.com/: ``` +# Troubleshooting + +## Failed to Authenticate, Received Status Code 401 from Keyfactor Command + +If you see this error, the identity provider that issued credentials to your command-cert-manager-issuer (using OAuth, Basic, or ambient credentials) is not a registered identity provider in your Keyfactor Command instance. Please see the [Configuring Command](#configuring-command) section for more information. + +```bash +failed to create new Command API client: failed to authenticate, received status code 401 from Keyfactor Command +``` + +## Failed to Authenticate, Received Status Code 403 from Keyfactor Command + +If you see this error, the identity provider that issued credentials to your command-cert-manager-issuer (using OAuth, Basic, or ambient credentials) is configured in Keyfactor Command, however the identity associated to those credentials is not associated with any security roles. Make sure the identity is mapped to a security claim. See the **Configure Command Security Roles and Claims** section of the [Configuring Command](#configuring-command) section for more information. + +```bash +failed to create new Command API client: failed to authenticate, received status code 403 from Keyfactor Command: {\"ErrorCode\":\"0xA0140002\",\"Message\":\"User doesn\\u0027t have the required permission\"} +``` + +If you see this sort of error, the identity is mapped to one or more security roles in Keyfactor Command, but is missing the necessary permissions. See the **Configure Command Security Roles and Claims** section of the [Configuring Command](#configuring-command) section for the required permissions. + +```bash +failed to fetch metadata fields from connected Command instance: User does not have the required permissions: /metadata/types/read/. +``` + ## License diff --git a/api/v1alpha1/issuer_types.go b/api/v1alpha1/issuer_types.go index 98f604f..2179c34 100644 --- a/api/v1alpha1/issuer_types.go +++ b/api/v1alpha1/issuer_types.go @@ -1,5 +1,5 @@ /* -Copyright © 2024 Keyfactor +Copyright © 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -46,7 +46,23 @@ type IssuerSpec struct { // +kubebuilder:default:=KeyfactorAPI APIPath string `json:"apiPath,omitempty"` - // CertificateTemplate is the name of the certificate template to use. + // EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + // If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + // If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. + // Enrollment will fail if the specified template is not compatible with the enrollment pattern. + // Refer to the Keyfactor Command documentation for more information. + EnrollmentPatternId int32 `json:"enrollmentPatternId,omitempty"` + + // EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + // If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + // If EnrollmentPatternId and EnrollmentPatternName are both specified, EnrollmentPatternId will take precedence. + // Enrollment will fail if the specified template is not compatible with the enrollment pattern. + // Refer to the Keyfactor Command documentation for more information. + EnrollmentPatternName string `json:"enrollmentPatternName,omitempty"` + + // Deprecated. CertificateTemplate is the name of the certificate template to use. If using Keyfactor Command 25.1 or later, use EnrollmentPatternName or EnrollmentPatternId instead. + // If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + // Enrollment will fail if the specified template is not compatible with the enrollment pattern. // Refer to the Keyfactor Command documentation for more information. CertificateTemplate string `json:"certificateTemplate,omitempty"` diff --git a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml index 452fc46..00e7b83 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_clusterissuers.yaml @@ -68,9 +68,28 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + enrollmentPatternId: + description: |- + EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: integer + format: int32 + enrollmentPatternName: + description: |- + EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string certificateTemplate: description: |- - CertificateTemplate is the name of the certificate template to use. + CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: diff --git a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml index f06ddbb..b87e277 100644 --- a/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml +++ b/config/crd/bases/command-issuer.keyfactor.com_issuers.yaml @@ -68,9 +68,28 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + enrollmentPatternId: + description: |- + EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: integer + format: int32 + enrollmentPatternName: + description: |- + EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string certificateTemplate: description: |- - CertificateTemplate is the name of the certificate template to use. + CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: 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 40bd6c8..011d8f2 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/clusterissuers.yaml @@ -62,9 +62,28 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + enrollmentPatternId: + description: |- + EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: integer + format: int32 + enrollmentPatternName: + description: |- + EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string certificateTemplate: description: |- - CertificateTemplate is the name of the certificate template to use. + CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: 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 879c88f..8b7ac01 100644 --- a/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml +++ b/deploy/charts/command-cert-manager-issuer/templates/crds/issuers.yaml @@ -62,9 +62,28 @@ spec: CertificateAuthorityLogicalName is the logical name of the certificate authority to use E.g. "Keyfactor Root CA" or "Intermediate CA" type: string + enrollmentPatternId: + description: |- + EnrollmentPatternId is the ID of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: integer + format: int32 + enrollmentPatternName: + description: |- + EnrollmentPatternName is the name of the enrollment pattern to use. Supported in Keyfactor Command 25.1 and later. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + If both enrollmentPatternId and enrollmentPatternName are specified, enrollmentPatternId will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. + Refer to the Keyfactor Command documentation for more information. + type: string certificateTemplate: description: |- - CertificateTemplate is the name of the certificate template to use. + CertificateTemplate is the name of the certificate template to use. Deprecated in favor of EnrollmentPattern as of Keyfactor Command 25.1. + If both enrollment pattern and certificate template are specified, enrollment pattern will take precedence. + Enrollment will fail if the specified template is not compatible with the enrollment pattern. Refer to the Keyfactor Command documentation for more information. type: string commandSecretName: diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6c5c44d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Keyfactor Docs + +This is supplemental documentation for the [Keyfactor Command Cert Manager Issuer](../README.md). Please refer to the root-level README as the entrypoint for documentation regarding this integration. \ No newline at end of file diff --git a/docs/ambient-providers/azure.md b/docs/ambient-providers/azure.md new file mode 100644 index 0000000..22a27af --- /dev/null +++ b/docs/ambient-providers/azure.md @@ -0,0 +1,333 @@ +# Managed Identity Using Azure Entra ID Workload Identity (AKS) + +This documentation is for instructions on using ambient credentials within Azure Kubernetes Services (AKS). Full documentation on Command Cert Manager Issuer can be found [here](../../README.md). + +## Prerequisites + +- [kubectl](https://kubernetes.io/docs/reference/kubectl/) installed on your machine and [connected to your AKS cluster](https://learn.microsoft.com/en-us/azure/aks/learn/quick-kubernetes-deploy-cli#connect-to-the-cluster) +- [Helm](https://github.com/helm/helm?tab=readme-ov-file#install) 3.x installed +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) installed and logged in + +## Background + +There are two types of [managed identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview#managed-identity-types) that your Azure AKS workload may use: +- System-assigned managed identity (MSI) + - Automatically created and managed by Azure at the cluster level. This identity **can not** be shared with other Azure resources. This is used by default. +- User-assigned managed identity (UAMI) + - Created and managed by you. Identity **can** be shared with other Azure resources and associated with Kubernetes ServiceAccounts via Azure AD Workload Identity. Requires explicit workload identity configuration (show below). + +Since you are using ambient credentials generated by your Azure AKS workload and targeting these credentials for your Command instance, you will need to create an [Azure App Registration](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app). We will walk through App Registration configuration in this document. + +## MSI vs UAMI: Which to use + +While system-assigned managed identity (MSI) is easy to use and enabled by default, user-assigned managed identity (UAMI) is the ***recommended identity type*** to use for your workload. UAMI identities can be shared with other AKS clusters and workloads, and offer more control over how the identity is used. If your app registration [requires a role assignment](#app-registration-assignment-requirement), you **must** use a UAMI. An MSI **cannot** be assigned to an app registration role. + +### Quick Decision Guide + +| Scenario | Recommended Identity Type | +|----------|-------------------------| +| Simple setup, single cluster | System-Assigned (MSI) | +| Multiple clusters need same identity | User-Assigned (UAMI) | +| App registration requires role assignment | User-Assigned (UAMI) - **Required** | +| Production environments | User-Assigned (UAMI) | +| Development/testing | Either (MSI for simplicity) | + + +## System-Assigned Managed Identity (MSI) + +By default, your AKS cluster is configured to use system-assigned managed identity. Your workload should automatically use the identity assigned to the cluster. You will need to set up the scope of the issuer to reference an app registration. Lastly, you will need to make sure the object ID of the managed identity is associated to a security claim in Keyfactor Command. + +1. Install `cert-manager` to your AKS cluster. [Installation steps](https://cert-manager.io/docs/installation/helm/) +1. Install `command-cert-manager-issuer` to your AKS cluster. [Installation steps](../../README.md#installing-command-issuer) +1. Create an Azure App Registration. [Installation steps](#azure-app-registration) +1. Deploy Issuer or ClusterIssuer Resource. [Installation steps](../../README.md#creating-issuer-and-clusterissuer-resources) + - To use ambient credentials, do not supply a `commandSecretName` to your issuer's specification. + - **IMPORTANT**: Fill in the `scopes` in your issuer's specification with the Application ID URI of your App Registration, suffixed with `./default`. Example: + ```yaml + # Example issuer configuration + spec: + scopes: "api://your-app-registration-id/.default" + ``` +1. Add the system-assigned managed identity object ID to a security claim in Keyfactor Command + ```bash + export AKS_CLUSTER_RESOURCE_GROUP="" # the resource group your AKS cluster is deployed to + export AKS_CLUSTER_NAME="" # the name of your AKS cluster + export CURRENT_TENANT=$(az account show --query tenantId --output tsv) + + echo "AKS Cluster Resource Group: $AKS_CLUSTER_RESOURCE_GROUP" + echo "AKS Cluster Name: $AKS_CLUSTER_NAME" + + # Get the principal ID of your AKS cluster + AKS_CLUSTER_OBJECT_ID=$(az aks show --resource-group $AKS_CLUSTER_RESOURCE_GROUP --name $AKS_CLUSTER_NAME --query "identityProfile.kubeletidentity.objectId" -o tsv) + echo "AKS Cluster MSI Object ID: $AKS_CLUSTER_OBJECT_ID" + + echo "View then OIDC configuration for the Entra OIDC token issuer: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0/.well-known/openid-configuration" + + echo "Authority: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0" + ``` + + > **Note**: AKS workloads inherit the kubelet's managed identity, not the cluster's control plane identity. This is why we use `identityProfile.kubeletidentity.objectId` rather than `identity.principalId`. + + You can map the object ID to an OAuth Subject or OAuth Object ID security claim in Keyfactor Command. Make sure the [security claim is associated to a security role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) with the required permissions. Please refer to the [Configuring Command](../../README.md#configuring-command) **Configure Command Security Roles and Claims** section for security role requirements. + + Make sure an identity provider is configured in Keyfactor Command with the authority set to the authority output above. + +## User-Assigned Managed Identity (UAMI) + +User-assigned managed identity configuration is more involved, but allows the identity to be shared across different AKS clusters. The AKS cluster will need to be configured to allow workload identity and the Command Issuer's ServiceAccount will need to reference the client ID of the user-assigned managed identity. You will need to make sure the principal ID of the user-assigned managed identity is associated to a security claim in Keyfactor Command. + +1. Install `cert-manager` to your AKS cluster. [Installation steps](https://cert-manager.io/docs/installation/helm/) +1. Enable OIDC and Workload Identity on your AKS cluster. [Learn more](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster) + ```bash + export AKS_CLUSTER_RESOURCE_GROUP="" # the resource group your AKS cluster is deployed to + export AKS_CLUSTER_NAME="" # the name of your AKS cluster + + echo "AKS Cluster Resource Group: $AKS_CLUSTER_RESOURCE_GROUP" + echo "AKS Cluster Name: $AKS_CLUSTER_NAME" + + echo "Enabling OIDC and workload identity on AKS cluster..." + + az aks update \ + --name ${AKS_CLUSTER_NAME} \ + --resource-group ${AKS_CLUSTER_RESOURCE_GROUP} \ + --enable-oidc-issuer \ + --enable-workload-identity + ``` +1. Create a user-assigned managed identity + ```bash + export UAMI_IDENTITY_NAME="command-issuer-uami" # the name you want to give your UAMI + + echo "Creating user assigned managed identity $UAMI_IDENTITY_NAME..." + + az identity create --name "${UAMI_IDENTITY_NAME}" --resource-group "${AKS_CLUSTER_RESOURCE_GROUP}" + + export UAMI_CLIENT_ID=$(az identity show --name $UAMI_IDENTITY_NAME --resource-group $AKS_CLUSTER_RESOURCE_GROUP --query clientId --output tsv) + + echo "Client ID of user-assigned managed identity: $UAMI_CLIENT_ID" + ``` +1. Deploy Command Cert Manager Issuer with ServiceAccount labeled to use workload identity and UAMI client ID + + ```bash + export UAMI_CLIENT_ID=$(az identity show --name $UAMI_IDENTITY_NAME --resource-group $AKS_CLUSTER_RESOURCE_GROUP --query clientId --output tsv) # should be the same as the previous step + + export ISSUER_NAMESPACE="command-issuer-system" + + echo "Installing command-cert-manager issuer to namespace $ISSUER_NAMESPACE" + echo "Labeling ServiceAccount to use workload identity with user-assigned-managed-identity client ID $UAMI_CLIENT_ID..." + + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace $ISSUER_NAMESPACE \ + --create-namespace \ + --set "fullnameOverride=command-cert-manager-issuer" \ + --set-string "podLabels.azure\.workload\.identity/use=true" \ + --set-string "serviceAccount.labels.azure\.workload\.identity/use=true" \ + --set-string "serviceAccount.annotations.azure\.workload\.identity/client-id=${UAMI_CLIENT_ID}" + ``` + + + If successful, the Command Issuer Pod will have new environment variables and the Azure WI ServiceAccount token as a projected volume: + + ```shell + kubectl -n command-issuer-system describe pod + ``` + + ```shell + Containers: + command-cert-manager-issuer: + ... + Environment: + AZURE_CLIENT_ID: + AZURE_TENANT_ID: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_AUTHORITY_HOST: https://login.microsoftonline.com/ + Mounts: + /var/run/secrets/azure/tokens from azure-identity-token (ro) + /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6rmzz (ro) + ... + Volumes: + ... + azure-identity-token: + Type: Projected (a volume that contains injected data from multiple sources) + TokenExpirationSeconds: 3600 + ``` +1. Associate a Federated Identity Credential (FIC) with the User Assigned Managed Identity. The FIC allows Command Issuer to act on behalf of the Managed Identity by telling Azure to expect: + - The `iss` claim of the ServiceAccount token to match the cluster's OIDC Issuer. Azure will also use the Issuer URL to download the JWT signing certificate. + - The `sub` claim of the ServiceAccount token to match the ServiceAccount's name and namespace. + + ```shell + export SERVICE_ACCOUNT_NAME=command-cert-manager-issuer # This is the default Kubernetes ServiceAccount used by the Command Issuer controller. + export SERVICE_ACCOUNT_NAMESPACE=command-issuer-system # This is the default namespace for Command Issuer used in this doc. + + export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $AKS_CLUSTER_RESOURCE_GROUP --name $AKS_CLUSTER_NAME --query "oidcIssuerProfile.issuerUrl" -o tsv) + + echo "Service account issuer: $SERVICE_ACCOUNT_ISSUER" + echo "Creating federated credentials for user-assigned managed identity $UAMI_IDENTITY_NAME in resource group $AKS_CLUSTER_RESOURCE_GROUP..." + + az identity federated-credential create \ + --name "${UAMI_IDENTITY_NAME}-federated-credentials" \ + --identity-name "${UAMI_IDENTITY_NAME}" \ + --resource-group "${AKS_CLUSTER_RESOURCE_GROUP}" \ + --issuer "${SERVICE_ACCOUNT_ISSUER}" \ + --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" \ + --audiences "api://AzureADTokenExchange" + ``` + + > Read more about [Workload Identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) in the Entra ID documentation. + > + > Read more about [the `az identity federated-credential` command](https://learn.microsoft.com/en-us/cli/azure/identity/federated-credential?view=azure-cli-latest). +1. Create an Azure App Registration. [Installation steps](#azure-app-registration) +1. Deploy Issuer or ClusterIssuer Resource. [Installation steps](../../README.md#creating-issuer-and-clusterissuer-resources) + - To use ambient credentials, do not supply a `commandSecretName` to your issuer's specification. + - **IMPORTANT**: Fill in the `scopes` in your issuer's specification with the Application ID URI of your App Registration, suffixed with `./default`. Example: + ```yaml + # Example issuer configuration + spec: + scopes: "api://your-app-registration-id/.default" + ``` +1. Add the user-assigned managed identity principal ID to a security claim in Keyfactor Command + ```shell + export UAMI_PRINCIPAL_ID=$(az identity show --name $UAMI_IDENTITY_NAME --resource-group $AKS_CLUSTER_RESOURCE_GROUP --query principalId --output tsv) + export CURRENT_TENANT=$(az account show --query tenantId --output tsv) + echo "UAMI Principal ID: ${UAMI_PRINCIPAL_ID}" + + echo "View then OIDC configuration for the Entra OIDC token issuer: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0/.well-known/openid-configuration" + + echo "Authority: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0" + ``` + + You can map the principal ID to an OAuth Subject or OAuth Object ID security claim in Keyfactor Command. Make sure the [security claim is associated to a security role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) with the required permissions. Please refer to the [Configuring Command](../../README.md#configuring-command) **Configure Command Security Roles and Claims** section for security role requirements. + + Make sure an identity provider is configured in Keyfactor Command with the authority set to the authority output above. + + +## Azure App Registration + +The identity server that generates the access token from DefaultAzureCredentials requires a valid scope. The scope supplied to DefaultAzureCredentials sets the audience claim of the access token. The access token is being used for authorization on a resource outside of Azure (Keyfactor Command), so an app registration for Entra AD to represent an external application. + +Here is official Azure documentation on how to [create an app registration](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app). + +After the App Registration is created, expose an API. You can do this by going to Manage > Expose an API and editing the Application ID URI. + +> IMPORTANT: The Application ID URI will be used in your `scopes` claim. Make sure to copy this value down. For example, if your Application ID URI is `api://abcd`, your scope value should be `api://abcd/.default`. + +![Application ID URI](../assets/app-registration-app-id-uri.png) + +### App Registration Assignment Requirement + +By default, Azure App Registrations do not require an assignment in order for an identity to access to the application. However, there may be some compliance need to require an assignment for an identity to access your app registration. This option can be toggled via the Enterprise Application properties of your App Registration. If enabled, you **must use** a user-assigned managed identity for your workload (a system-assigned managed identity cannot be assigned a role). If this identity does not have a role assignment to the app registration, you may see the error: + +```bash +AADSTS501051: Application ''() is not assigned to a role for the application 'api://'() +``` + +![App Registration Assignment Required](../assets/app-registration-assignment-required.png) + +> If the UAMI identity is tied to an app registration role, the name of the security role can be added as a security claim in Keyfactor Command. Then, the identity can assume any Keyfactor Command security role with that security claim assigned to it. + +You can assign the identity to an app registration role from the Enterprise Application. Please refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-app-roles-in-apps#assign-users-and-groups-to-microsoft-entra-roles) for more information. + +For more information about the assignment requirement for app registrations and how this can affect your identities, please see [this blog post](https://mderriey.com/2019/04/19/aad-apps-user-assignment-required/). + +## Troubleshooting + +This troubleshooting section is intended for issues specific to the Azure AKS environment. If you do not see your issue in these troubleshooting steps, please see the troubleshooting steps in the [directory root](../../README.md#troubleshooting). + +### Common Pitfalls + +1. **Forgetting the `/.default` suffix** in scopes configuration +1. **Using wrong object ID** - If using MSI, MSI uses kubelet identity, not the cluster identity +1. **ServiceAccount mismatch** - If using UAMI, federated credentials must exactly match ServiceAccount name/namespace + +### Determining Which Managed Identity Your AKS Workload is Using + +Azure has documentation around [determining the managed identity a cluster is using](https://learn.microsoft.com/en-us/azure/aks/use-managed-identity#determine-which-type-of-managed-identity-a-cluster-is-using), but this section will confirm if your AKS workload is using UAMI or MSI for its managed identity. + +#### Determine if workload identity is enabled on the AKS cluster + +```bash +az aks show –-resource-group [group] –-name [name] --query "[oidcIssuerProfile,securityProfile]" +``` + +If you see something like this, your AKS cluster **has workload identity enabled**: + +```json +[ + { + "enabled": true, + "issuerUrl": "https://" + }, + { + "azureKeyVaultKms": null, + "defender": null, + "imageCleaner": null, + "workloadIdentity": { + "enabled": true + } + } +] +``` + +#### Check if the ServiceAccount is annotated with a client ID and workload is enabled. + +Run this script to see if your ServiceAccount is annotated with the client ID of the UAMI and workload identity is enabled. + +```bash +kubectl describe serviceaccount --namespace +... +Labels: ... + azure.workload.identity/use=true +Annotations: azure.workload.identity/client-id: + ... +Image pull secrets: +Mountable secrets: +Tokens: +Events: +``` + +#### Check if your Kubernetes pod is labeled to use workload identity + +Run this script to see if your Kubernetes pod is running workload identity enabled. + +```bash +kubectl get pods --namespace --show-labels +NAME READY STATUS RESTARTS AGE LABELS +command-issuer-86c4fdfb67-h4vqb 1/1 Running 0 105s app.kubernetes.io/instance=cert-manager-issuer,app.kubernetes.io/name=command-cert-manager-issuer,azure.workload.identity/use=true,pod-template-hash=86c4fdfb67 +``` + +#### Conclusion + +If all of the above steps indicate your cluster has workload identity enabled, the pod is labeled to use workload identity, and the ServiceAccount is annotated with the UAMI client ID, your workload is most likely using **user-assigned managed identity**. + +### Required Query Variable 'Resource' Is Missing + +If you see the following error, this indicates your issuer / cluster issuer is missing a `scopes` field in its spec. DefaultAzureCredentials requires a valid scope, which should reference the [app registration](#azure-app-registration). + +```bash +failed to authenticate a system assigned identity. The endpoint responded with {\"error\":\"invalid_request\",\"error_description\":\"Required query variable 'resource' is missing\"} +``` + +### AADSTS500011: Resource principal named was not found in the tenant + +If you see the following error, this indicates the `scopes` specification on your issuer / cluster issuer is present but pointing to an invalid resource (make sure it's pointing to the [app registration](#azure-app-registration) application ID URI). + +```bash +AADSTS500011: The resource principal named was not found in the tenant named . This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant +``` + +If the `scopes` field is set to a valid application ID URI, make sure you are targeting the `/.default` suffix. + +### AADSTS501051: Application is not assigned to a role + +If you see the following error, this indicates the identity you're using does not have a role assignment to an app registration that requires a role assignment. See [this section](#app-registration-assignment-requirement) for more details. + +```bash +AADSTS501051: Application ''() is not assigned to a role for the application 'api://'() +``` + +### AADSTS700213: No matching federated identity record found for presented assertion subject + +If you see the following error, the user-assigned managed identity (UAMI) is assigned to the command issuer's Kubernete ServiceAccount and is trying to use it. However, the UAMI is missing a federated credential that trusts the ServiceAccount. Please refer to the [user-assigned managed identity](#user-assigned-managed-identity-uami) section and check for the instructions on creating a federated identity credential. The federeated credential **must match** the Kubernetes service account's name and namespace. + +```bash +AADSTS700213: No matching federated identity record found for presented assertion subject 'system:serviceaccount::'. Check your federated identity credential Subject, Audience and Issuer against the presented assertion. https://learn.microsoft.com/entra/workload-id/workload-identity-federation +``` \ No newline at end of file diff --git a/docs/assets/app-registration-app-id-uri.png b/docs/assets/app-registration-app-id-uri.png new file mode 100644 index 0000000..7dfc8f3 Binary files /dev/null and b/docs/assets/app-registration-app-id-uri.png differ diff --git a/docs/assets/app-registration-assignment-required.png b/docs/assets/app-registration-assignment-required.png new file mode 100644 index 0000000..0841f79 Binary files /dev/null and b/docs/assets/app-registration-assignment-required.png differ diff --git a/docsource/content.md b/docsource/content.md index eb1aec6..4179942 100644 --- a/docsource/content.md +++ b/docsource/content.md @@ -12,6 +12,7 @@ Before continuing, ensure that the following requirements are met: - `/Status/Endpoints` - `/Enrollment/CSR` - `/MetadataFields` + - `/EnrollmentPatterns` (Keyfactor Command 25.1 and above) - Kubernetes >= v1.19 - [Kubernetes](https://kubernetes.io/docs/tasks/tools/), [Minikube](https://minikube.sigs.k8s.io/docs/start/), [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/), etc. > You must have permission to create [Custom Resource Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) in your Kubernetes cluster. @@ -22,7 +23,7 @@ Before continuing, ensure that the following requirements are met: ## Configuring Command -Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template suitable for your usecase. Additionally, you should ensure that the identity used by the Issuer/ClusterIssuer has the appropriate permissions in Command. +Command Issuer enrolls certificates by submitting a POST request to the Command CSR Enrollment endpoint. Before using Command Issuer, you must create or identify a Certificate Authority _and_ Certificate Template / Enrollment Pattern suitable for your use case. Additionally, you should ensure that the [identity provider](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/AuthenticateAPI.htm#AuthenticatingtotheKeyfactorAPI) used by the Issuer/ClusterIssuer has the appropriate permissions in Command. 1. **Create or identify a Certificate Authority** @@ -32,32 +33,37 @@ Command Issuer enrolls certificates by submitting a POST request to the Command The CA that you choose must be configured to allow CSR Enrollment. -2. **Identify a Certificate Template** +2. **Identify a Certificate Template / Enrollment Pattern** + + Keyfactor Command 25.1 introduces support for [Enrollment Patterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReleaseNotes/Release2511.htm#Highlights), which allow an easy way to share certificate configuration without requiring multiple certificate templates. Certificate Template configuration has been moved to the Enrollment Patterns screen. Certificate Templates will still be supported in Issuer / ClusterIssuer configuration, but it is recommended to start using Enrollment Patterns for Keyfactor Command versions 25.1 and above. + + - If you don't have any suitable Enrollment Patterns, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) or reach out to your Keyfactor support representative to learn more. Certificate Templates in Command define properties and constraints of the certificates being issued. This includes settings like key usage, extended key usage, validity period, allowed key algorithms, and signature algorithms. They also control the type of information that end entities must provide and how that information is validated before issuing certificates. - If you don't have any suitable Certificate Templates, refer to the [Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Configuring%20Template%20Options.htm?Highlight=Certificate%20Template) or reach out to your Keyfactor support representative to learn more. - The Certificate Template that you choose must be configured to allow CSR Enrollment. + The Certificate Template / Enrollment Pattern that you choose must be configured to allow CSR Enrollment. - You should make careful note of the allowed Key Types and Key Sizes on the Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the key `algorithm` and `size` are allowed by your Certificate Template in Command. + You should make careful note of the allowed Key Types and Key Sizes on the Certificate Template / Enrollment Pattern. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the key `algorithm` and `size` are allowed by your Certificate Template / Enrollment Pattern in Command. - The same goes for **Enrollment RegExes** and **Policies** defined on your Certificate Template. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the `subject`, `commonName`, `dnsNames`, etc. are allowed and/or configured correctly by your Certificate Template in Command. + The same goes for **Enrollment RegExes** and **Policies** defined on your Certificate Template / Enrollment Pattern. When creating cert-manager [Certificates](https://cert-manager.io/docs/usage/certificate/), you must make sure that the `subject`, `commonName`, `dnsNames`, etc. are allowed and/or configured correctly by your Certificate Template / Enrollment Pattern in Command. 3. **Configure Command Security Roles and Claims** In Command, Security Roles define groups of users or administrators with specific permissions. Users and subjects are identified by Claims. By adding a Claim to a Security Role, you can define what actions the user or subject can perform and what parts of the system it can interact with. - The security role will need to be added as an Allowed Requester Security Role on the Certificate Authority and Certificate Template configured in the previous two steps. + The security role will need to be added as an **Allowed Requester Security Role** on the Certificate Authority and Certificate Template / Enrollment Pattern configured in the previous two steps. - If you haven't created Roles and Access rules before, [this guide](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) provides a primer on these concepts in Command. If your security policy requires fine-grain access control, Command Issuer requires the following Access Rules: - | Global Permissions | Permission Model (Version Two) | Permission Model (Version One) | - |-----------------------------------------|---|---| - | Metadata > Types > Read | `/metadata/types/read/` | `CertificateMetadataTypes:Read` | - | Certificates > Enrollment > Csr | `/certificates/enrollment/csr/` | `CertificateEnrollment:EnrollCSR` | + | Global Permissions | Permission Model (Version Two) | Permission Model (Version One) | Notes + |-----------------------------------------|---|---|--| + | Metadata > Types > Read | `/metadata/types/read/` | `CertificateMetadataTypes:Read` | | + | Certificates > Enrollment > Csr | `/certificates/enrollment/csr/` | `CertificateEnrollment:EnrollCSR` | | + | Enrollment Patterns > Read (Optional) | `/enrollment_pattern/read/` | N/A | Required if using `EnrollmentPatternName` | > Documentation for [Version Two Permission Model](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityRolePermissions.htm#VersionTwoPermissionModel) and [Version One Permission Model](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityRolePermissions.htm#VersionOnePermissionModel) @@ -69,6 +75,8 @@ Command Issuer enrolls certificates by submitting a POST request to the Command ![Certificate Template Allowed Requester](./docsource/images/cert_template_allowed_requester.png) +![Enrollment Pattern Allowed Requester](./docsource/images/enrollment_pattern_allowed_requester.png) + ## Installing Command Issuer @@ -95,22 +103,37 @@ Command Issuer is installed using a Helm chart. The chart is available in the [C --create-namespace ``` + Optionally, set the Docker image tag of command-cert-manager-issuer to deploy ([available tags](https://hub.docker.com/r/keyfactor/command-cert-manager-issuer/tags)) + + ```shell + helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ + --namespace command-issuer-system \ + --set "image.tag=latest" \ + --create-namespace + ``` + > The Helm chart installs the Command Issuer CRDs by default. The CRDs can be installed manually with the `make install` target. # Authentication -Command Issuer supports authentication to Command using one of the following methods: +## Explicit Credentials + +Command Issuer supports explicit credentials authentication to Command using one of the following methods: -- Basic Authentication (username and password) -- OAuth 2.0 "client credentials" token flow (sometimes called two-legged OAuth 2.0) +- [Basic Authentication](#basic-auth) (username and password) +- [OAuth 2.0 "client credentials" token flow](#oauth) (sometimes called two-legged OAuth 2.0) These credentials must be configured using a Kubernetes Secret. By default, the secret is expected to exist in the same namespace as the issuer controller (`command-issuer-system` by default). > Command Issuer can read secrets in the Issuer namespace if `--set "secretConfig.useClusterRoleForSecretAccess=true"` flag is set when installing the Helm chart. +## Ambient Credentials + Command Issuer also supports ambient authentication, where a token is fetched from an Authorization Server using a cloud provider's auth infrastructure and passed to Command directly. The following methods are supported: -- Managed Identity Using Azure Entra ID Workload Identity (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) +- [Managed Identity Using Azure Entra ID Workload Identity](./docs/ambient-providers/azure.md) (if running in [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service)) + +If you are running your Kubernetes workload in a cloud provider not listed above, you can use workload identity federation with [Azure AD](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation). ## Basic Auth @@ -151,157 +174,7 @@ kubectl -n command-issuer-system create secret generic command-secret \ ## Managed Identity Using Azure Entra ID Workload Identity (AKS) -Azure Entra ID workload identity in Azure Kubernetes Service (AKS) allows Command Issuer to exchange a Kubernetes ServiceAccount Token for an Azure Entra ID access token, which is then used to authenticate to Command. - -At this time, Azure Kuberentes Services workload identity federation is best supported by [User Assigned Managed Identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-manage-user-assigned-managed-identities?pivots=identity-mi-methods-azp). Other identity solutions such as Azure AD Service Principals are not supported. - -Here is a guide on how to use Azure User Assigned Managed Identities to authenticate your AKS workload with your Keyfactor Command instance. - -1. Reconfigure the AKS cluster to enable workload identity federation. - - ```shell - export CLUSTER_NAME= - export RESOURCE_GROUP= - az aks update \ - --name ${CLUSTER_NAME} \ - --resource-group ${RESOURCE_GROUP} \ - --enable-oidc-issuer \ - --enable-workload-identity - ``` - - > The [Azure Workload Identity extension can be installed on non-AKS or self-managed clusters](https://azure.github.io/azure-workload-identity/docs/installation.html) if you're not using AKS. - > - > Refer to the [AKS documentation](https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster) for more information on the `--enable-workload-identity` feature. - -2. Create a User Assigned Managed Identity in Azure. - - ```shell - export IDENTITY_NAME=command-issuer - az identity create --name "${IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP}" - ``` - > Read more about [the `az identity` command](https://learn.microsoft.com/en-us/cli/azure/identity?view=azure-cli-latest). - -3. Reconfigure or deploy Command Issuer with extra labels for the Azure Workload Identity webhook, which will result in the Command Issuer controller Pod having an extra volume containing a Kubernetes ServiceAccount token which it will exchange for a token from Azure. - - ```shell - export UAMI_CLIENT_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query clientId --output tsv) - - echo "Identity Client ID: ${UAMI_CLIENT_ID}" - - helm install command-cert-manager-issuer command-issuer/command-cert-manager-issuer \ - --namespace command-issuer-system \ - --create-namespace \ - --set "fullnameOverride=command-cert-manager-issuer" \ - --set-string "podLabels.azure\.workload\.identity/use=true" \ - --set-string "serviceAccount.labels.azure\.workload\.identity/use=true" \ - --set-string "serviceAccount.annotations.azure\.workload\.identity/client-id=${UAMI_CLIENT_ID}" - ``` - - If successful, the Command Issuer Pod will have new environment variables and the Azure WI ServiceAccount token as a projected volume: - - ```shell - kubectl -n command-issuer-system describe pod - ``` - - ```shell - Containers: - command-cert-manager-issuer: - ... - Environment: - AZURE_CLIENT_ID: - AZURE_TENANT_ID: - AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token - AZURE_AUTHORITY_HOST: https://login.microsoftonline.com/ - Mounts: - /var/run/secrets/azure/tokens from azure-identity-token (ro) - /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-6rmzz (ro) - ... - Volumes: - ... - azure-identity-token: - Type: Projected (a volume that contains injected data from multiple sources) - TokenExpirationSeconds: 3600 - ``` - - > Refer to [Azure Workload Identity docs](https://azure.github.io/azure-workload-identity/docs/installation/mutating-admission-webhook.html) more information on the role of the Mutating Admission Webhook. - -4. Associate a Federated Identity Credential (FIC) with the User Assigned Managed Identity. The FIC allows Command Issuer to act on behalf of the Managed Identity by telling Azure to expect: - - The `iss` claim of the ServiceAccount token to match the cluster's OIDC Issuer. Azure will also use the Issuer URL to download the JWT signing certificate. - - The `sub` claim of the ServiceAccount token to match the ServiceAccount's name and namespace. - - ```shell - export SERVICE_ACCOUNT_NAME=command-cert-manager-issuer # This is the default Kubernetes ServiceAccount used by the Command Issuer controller. - export SERVICE_ACCOUNT_NAMESPACE=command-issuer-system # This is the default namespace for Command Issuer used in this doc. - - export SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --query "oidcIssuerProfile.issuerUrl" -o tsv) - az identity federated-credential create \ - --name "${IDENTITY_NAME}-federated-credentials" \ - --identity-name "${IDENTITY_NAME}" \ - --resource-group "${RESOURCE_GROUP}" \ - --issuer "${SERVICE_ACCOUNT_ISSUER}" \ - --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" \ - --audiences "api://AzureADTokenExchange" - ``` - - > Read more about [Workload Identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation) in the Entra ID documentation. - > - > Read more about [the `az identity federated-credential` command](https://learn.microsoft.com/en-us/cli/azure/identity/federated-credential?view=azure-cli-latest). - -5. Get the Managed Identity's Principal ID and Entra Identity Provider Information - - ```shell - export UAMI_PRINCIPAL_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RESOURCE_GROUP --query principalId --output tsv) - export CURRENT_TENANT=$(az account show --query tenantId --output tsv) - echo "UAMI Principal ID: ${UAMI_PRINCIPAL_ID}" - - echo "View then OIDC configuration for the Entra OIDC token issuer: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0/.well-known/openid-configuration" - - echo "Authority: https://login.microsoftonline.com/$CURRENT_TENANT/v2.0" - ``` - - > **IMPORTANT NOTE**: The Microsoft Entra Identity Provider is associated with your Azure tenant ID. Multi-tenant Azure workloads will require a Command Identity Provider for each tenant. - -6. Add the Microsoft Entra ID as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider) using the identity provider information from the previous step, and [add the Managed Identity's Principal ID as an `OAuth Subject` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. - -## Google Kubernetes Engine (GKE) Workload Identity - -Google Kuberentes Engine (GKE) supports the ability to authenticate your GKE workloads using workload identity. - -By default, GKE clusters are assigned the [default service account](https://cloud.google.com/compute/docs/access/service-accounts#token) for your Google project. This service account is used to generate an ID token for your workload. However, you may opt to use [Workload Identity Federation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#metadata-server) to your GKE cluster. - -1. Get the OAuth Client and Identity Provider for your GKE Cluster - - Regardless if you are using the default service account or a custom service account, the following script will help you derive your GKE cluster's OAuth Client: - - ```shell - export CLUSTER_NAME= - export GCLOUD_REGION= - export GCLOUD_PROJECT_ID=$(gcloud config get-value project) # populate with the current PROJECT_ID context - export GCLOUD_PROJECT_NUMBER=$(gcloud projects describe $GCLOUD_PROJECT_ID --format="value(projectNumber)") - - export GCLOUD_SERVICE_ACCOUNT=$(gcloud container clusters describe $CLUSTER_NAME \ - --zone $GCLOUD_REGION \ - --format="value(nodeConfig.serviceAccount)") - - if [[ "$GCLOUD_SERVICE_ACCOUNT" == "default" ]]; then - # Override service account with default compute service account - GCLOUD_SERVICE_ACCOUNT="$GCLOUD_PROJECT_NUMBER-compute@developer.gserviceaccount.com" - fi - - echo "Service account: $GCLOUD_SERVICE_ACCOUNT" - - # Get OAuth2 Client ID of service account - export GCLOUD_SERVICE_ACCOUNT_CLIENT_ID=$(gcloud iam service-accounts describe $GCLOUD_SERVICE_ACCOUNT \ - --format="value(oauth2ClientId)") - - echo "Service account OAuth2 client ID: $GCLOUD_SERVICE_ACCOUNT_CLIENT_ID" - - echo "View the OIDC configuration for Google's OIDC token issuer: https://accounts.google.com/.well-known/openid-configuration" - - echo "Authority: https://accounts.google.com" - ``` - -2. Add Google as an [Identity Provider in Command](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/IdentityProviders.htm?Highlight=identity%20provider) using the identity provider information from the previous step, and [add the Service Account's OAuth Client ID as an `OAuth Subject` claim to the Security Role](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/SecurityOverview.htm?Highlight=Security%20Roles) created/identified earlier. +This section has moved. Please refer to [this link](./docs/ambient-providers/azure.md) for documentation on configuring ambient credentials with AKS. # CA Bundle @@ -324,6 +197,8 @@ For example, ClusterIssuer resources can be used to issue certificates for resou export COMMAND_CA_HOSTNAME="" # Only required for non-HTTPS CA types export COMMAND_CA_LOGICAL_NAME="" export CERTIFICATE_TEMPLATE_SHORT_NAME="" + export ENROLLMENT_PATTERN_NAME="" + export ENROLLMENT_PATTERN_ID="" ``` The `spec` field of both the Issuer and ClusterIssuer resources use the following fields: @@ -331,12 +206,14 @@ For example, ClusterIssuer resources can be used to issue certificates for resou |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | hostname | The hostname of the Command API Server. | | apiPath | (optional) The base path of the Command REST API. Defaults to `KeyfactorAPI`. | - | commandSecretName | The name of the Kubernetes secret containing basic auth credentials or OAuth 2.0 credentials | + | commandSecretName | (optional) The name of the Kubernetes secret containing basic auth credentials or OAuth 2.0 credentials. Omit if using ambient credentials. | | caSecretName | (optional) The name of the Kubernetes secret containing the CA certificate. Required if the Command API uses a self-signed certificate or it was signed by a CA that is not widely trusted. | | certificateAuthorityLogicalName | The logical name of the Certificate Authority to use in Command. For example, `Sub-CA` | | certificateAuthorityHostname | (optional) The hostname of the Certificate Authority specified by `certificateAuthorityLogicalName`. This field is usually only required if the CA in Command is a DCOM (MSCA-like) CA. | - | certificateTemplate | The Short Name of the Certificate Template to use when this Issuer/ClusterIssuer enrolls CSRs. | - | scopes | (Optional) If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | + | enrollmentPatternId | The ID of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternId` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | + | enrollmentPatternName | The Name of the [Enrollment Pattern](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Enrollment-Patterns.htm) to use when this Issuer/ClusterIssuer enrolls CSRs. **Supported by Keyfactor Command 25.1 and above**. If `certificateTemplate` and `enrollmentPatternName` are both specified, the enrollment pattern parameter will take precedence. If `enrollmentPatternId` and `enrollmentPatternName` are both specified, `enrollmentPatternId` will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. If using `enrollmentPatternName`, your security role must have `/enrollment_pattern/read/` permission. | + | certificateTemplate | The Short Name of the Certificate Template to use when this Issuer/ClusterIssuer enrolls CSRs. **Deprecated in favor of [Enrollment Patterns](https://software.keyfactor.com/Core-OnPrem/Current/Content/WebAPI/KeyfactorAPI/Enrollment-Patterns.htm) as of Keyfactor Command 25.1**. If `certificateTemplate` and either `enrollmentPatternName` or `enrollmentPatternId` are specified, the enrollment pattern parameter will take precedence. Enrollment will fail if the specified certificate template is not compatible with the enrollment pattern. | + | scopes | (Optional) Required if using ambient credentials with Azure AKS. If using ambient credentials, these scopes will be put on the access token generated by the ambient credentials' token provider, if applicable. | | audience | (Optional) If using ambient credentials, this audience will be put on the access token generated by the ambient credentials' token provider, if applicable. Google's ambient credential token provider generates an OIDC ID Token. If this value is not provided, it will default to `command`. | > If a different combination of hostname/certificate authority/certificate template is required, a new Issuer or ClusterIssuer resource must be created. Each resource instantiation represents a single configuration. @@ -357,13 +234,15 @@ For example, ClusterIssuer resources can be used to issue certificates for resou spec: hostname: "$HOSTNAME" apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically - commandSecretName: "command-secret" # references the secret created above + commandSecretName: "command-secret" # references the secret created above. Omit if using ambient credentials. caSecretName: "command-ca-secret" # references the secret created above # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" - # scopes: "openid email https://example.com/.default" # Uncomment if desired + enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. + # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired EOF @@ -383,13 +262,15 @@ For example, ClusterIssuer resources can be used to issue certificates for resou spec: hostname: "$HOSTNAME" apiPath: "/KeyfactorAPI" # Preceding & trailing slashes are handled automatically - commandSecretName: "command-secret" # references the secret created above + commandSecretName: "command-secret" # references the secret created above. Omit if using ambient credentials. caSecretName: "command-ca-secret" # references the secret created above # certificateAuthorityHostname: "$COMMAND_CA_HOSTNAME" # Uncomment if required certificateAuthorityLogicalName: "$COMMAND_CA_LOGICAL_NAME" - certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" - # scopes: "openid email https://example.com/.default" # Uncomment if desired + enrollmentPatternId: "$ENROLLMENT_PATTERN_ID" # Only supported on Keyfactor Command 25.1 and above. + certificateTemplate: "$CERTIFICATE_TEMPLATE_SHORT_NAME" # Required if using Keyfactor Command 24.4 and below. + # enrollmentPatternName: "$ENROLLMENT_PATTERN_NAME" # Only supported on Keyfactor Command 25.1 and above. + # scopes: "openid email https://example.com/.default" # Uncomment if required # audience: "https://your-command-url.com" # Uncomment if desired EOF @@ -437,7 +318,7 @@ spec: request: ``` -> All fields in Command Issuer and ClusterIssuer `spec` can be overridden by applying Kubernetes Annotations to Certificates _and_ CertificateRequests. See [runtime customization for more](docs/annotations.md) +> All fields in Command Issuer and ClusterIssuer `spec` can be overridden by applying Kubernetes Annotations to Certificates _and_ CertificateRequests. See [runtime customization for more](#overriding-the-issuerclusterissuer-spec-using-kubernetes-annotations-on-certificaterequest-resources) ## Approving Certificate Requests @@ -459,11 +340,13 @@ kubectl get secret command-certificate -o jsonpath='{.data.tls\.crt}' | base64 - ## Overriding the Issuer/ClusterIssuer `spec` using Kubernetes Annotations on CertificateRequest Resources -Command Issuer allows you to override the `certificateAuthorityHostname`, `certificateAuthorityLogicalName`, and `certificateTemplate` by setting Kubernetes Annotations on CertificateRequest resources. This may be useful if certain enrollment scenarios require a different Certificate Authority or Certificate Template, but you don't want to create a new Issuer/ClusterIssuer. +Command Issuer allows you to override the `certificateAuthorityHostname`, `certificateAuthorityLogicalName`, `certificateTemplate`, `enrollmentPatternName`, and `enrollmentPatternId` by setting Kubernetes Annotations on CertificateRequest resources. This may be useful if certain enrollment scenarios require a different Certificate Authority or Certificate Template, but you don't want to create a new Issuer/ClusterIssuer. - `command-issuer.keyfactor.com/certificateAuthorityHostname` overrides `certificateAuthorityHostname` - `command-issuer.keyfactor.com/certificateAuthorityLogicalName` overrides `certificateAuthorityLogicalName` - `command-issuer.keyfactor.com/certificateTemplate` overrides `certificateTemplate` +- `command-issuer.keyfactor.com/enrollmentPatternName` overrides `enrollmentPatternName` +- `command-issuer.keyfactor.com/enrollmentPatternId` overrides `enrollmentPatternId`. Needs to be in string format. > cert-manager copies Annotations set on Certificate resources to the corresponding CertificateRequest. @@ -477,6 +360,8 @@ Command Issuer allows you to override the `certificateAuthorityHostname`, `certi > kind: Certificate > metadata: > annotations: +> command-issuer.keyfactor.com/enrollmentPatternId: "1234" +> command-issuer.keyfactor.com/enrollmentPatternName: "Kubernetes Enrollment Pattern" > command-issuer.keyfactor.com/certificateTemplate: "Ephemeral2day" > command-issuer.keyfactor.com/certificateAuthorityLogicalName: "InternalIssuingCA1" > metadata.command-issuer.keyfactor.com/ResponsibleTeam: "theResponsibleTeam@example.com" @@ -513,3 +398,27 @@ Keyfactor Command allows users to [attach custom metadata to certificates](https ```yaml metadata.command-issuer.keyfactor.com/: ``` + +# Troubleshooting + +## Failed to Authenticate, Received Status Code 401 from Keyfactor Command + +If you see this error, the identity provider that issued credentials to your command-cert-manager-issuer (using OAuth, Basic, or ambient credentials) is not a registered identity provider in your Keyfactor Command instance. Please see the [Configuring Command](#configuring-command) section for more information. + +```bash +failed to create new Command API client: failed to authenticate, received status code 401 from Keyfactor Command +``` + +## Failed to Authenticate, Received Status Code 403 from Keyfactor Command + +If you see this error, the identity provider that issued credentials to your command-cert-manager-issuer (using OAuth, Basic, or ambient credentials) is configured in Keyfactor Command, however the identity associated to those credentials is not associated with any security roles. Make sure the identity is mapped to a security claim. See the **Configure Command Security Roles and Claims** section of the [Configuring Command](#configuring-command) section for more information. + +```bash +failed to create new Command API client: failed to authenticate, received status code 403 from Keyfactor Command: {\"ErrorCode\":\"0xA0140002\",\"Message\":\"User doesn\\u0027t have the required permission\"} +``` + +If you see this sort of error, the identity is mapped to one or more security roles in Keyfactor Command, but is missing the necessary permissions. See the **Configure Command Security Roles and Claims** section of the [Configuring Command](#configuring-command) section for the required permissions. + +```bash +failed to fetch metadata fields from connected Command instance: User does not have the required permissions: /metadata/types/read/. +``` \ No newline at end of file diff --git a/docsource/images/enrollment_pattern_allowed_requester.png b/docsource/images/enrollment_pattern_allowed_requester.png new file mode 100644 index 0000000..33707f0 Binary files /dev/null and b/docsource/images/enrollment_pattern_allowed_requester.png differ diff --git a/e2e/.env.example b/e2e/.env.example new file mode 100644 index 0000000..874e5fa --- /dev/null +++ b/e2e/.env.example @@ -0,0 +1,10 @@ +export HOSTNAME="command.hostname.com" +export API_PATH="KeyfactorAPI" + +export CERTIFICATE_TEMPLATE="Server_tlsServerAuth-1y" +export CERTIFICATE_AUTHORITY_HOSTNAME="" +export CERTIFICATE_AUTHORITY_LOGICAL_NAME="Sub-CA" + +export OAUTH_TOKEN_URL="https://example.com/oauth2/token" +export OAUTH_CLIENT_ID="changeme" +export OAUTH_CLIENT_SECRET='changeme' \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..f4fbe62 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,7 @@ +# End-to-End Test Suite + +This is a test suite intended to make it easy to run tests on the command-cert-manager-issuer project. This suite can test the local changes of the command issuer, and it is able to test existing Docker images. + +This is currently configured as a Bash script, so it is necessary to run this on a UNIX-compatible machine. + +Instructions on how to run the e2e test suite are within the [run_tests.sh](./run_tests.sh) file. \ No newline at end of file diff --git a/e2e/run_tests.sh b/e2e/run_tests.sh new file mode 100755 index 0000000..f037381 --- /dev/null +++ b/e2e/run_tests.sh @@ -0,0 +1,600 @@ +#!/bin/bash + +## ======================= LICENSE =================================== +# Copyright © 2025 Keyfactor +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## ========================================================================== + +## ======================= Description =================================== + +# This script automates the deployment of the command-cert-manager-issuer +# and runs end-to-end tests to validate its functionality. +# This script is intended for use in a Minikube environment. +# This script can be run multiple times to test various scenarios. + +## ======================================================================= + +## ======================= Requirements =================================== +# - Minikube running +# - Helm installed +# - Docker installed +# - kubectl installed +# - cmctl installed +# - cert-manager Helm chart available +## =========================================================================== + +## ======================= How to run =================================== +# Enable the script to run: +# > chmod +x run_tests.sh +# Load the environment variables: +# > source .env +# Run the tests: +# > ./run_tests.sh +## =========================================================================== + + +IMAGE_REPO="keyfactor" +IMAGE_NAME="command-cert-manager-issuer" +# IMAGE_TAG="2.2.0-rc.9" # Uncomment if you want to use an existing image from the repository +IMAGE_TAG="local" # Uncomment if you want to build the image locally +FULL_IMAGE_NAME="${IMAGE_REPO}/${IMAGE_NAME}:${IMAGE_TAG}" + +HELM_CHART_NAME="command-cert-manager-issuer" +#HELM_CHART_VERSION="2.1.0" # Uncomment if you want to use a specific version from the Helm repository +HELM_CHART_VERSION="local" # Uncomment if you want to use the local Helm chart + +IS_LOCAL_DEPLOYMENT=$([ "$IMAGE_TAG" = "local" ] && echo "true" || echo "false") +IS_LOCAL_HELM=$([ "$HELM_CHART_VERSION" = "local" ] && echo "true" || echo "false") + +# TODO: Handle both in the e2e tests +ISSUER_TYPE="Issuer" +CLUSTER_ISSUER_TYPE="ClusterIssuer" + +#ISSUER_OR_CLUSTER_ISSUER="ClusterIssuer" +ISSUER_OR_CLUSTER_ISSUER="Issuer" +ISSUER_CR_NAME="issuer" +ISSUER_CRD_FQTN="issuers.command-issuer.keyfactor.com" +CLUSTER_ISSUER_CRD_FQTN="clusterissuers.command-issuer.keyfactor.com" + +ENROLLMENT_PATTERN_ID=1 +ENROLLMENT_PATTERN_NAME="Test Enrollment Pattern" + +CHART_PATH="./deploy/charts/command-cert-manager-issuer" + +CERT_MANAGER_VERSION="v1.17.0" + +MANAGER_NAMESPACE="command-issuer-system" +CERT_MANAGER_NAMESPACE="cert-manager" +ISSUER_NAMESPACE="issuer-playground" + +SIGNER_SECRET_NAME="auth-secret" +SIGNER_CA_SECRET_NAME="ca-secret" + +CERTIFICATEREQUEST_CRD_FQTN="certificaterequests.cert-manager.io" + + +CR_CR_NAME="req" + +set -e # Exit on any error + +validate_env_present() { + local env_var=$1 + local required=$2 + if [ -z "${!env_var}" ]; then + if [ "$required" = "false" ]; then + echo "ℹ️ Optional environment variable $env_var is not set. Continuing..." + return 0 + fi + echo "⚠️ Required environment variable $env_var. Please check your .env file or set it in your shell." + echo " Run: source .env or export $env_var=" + exit 1 + fi +} + +check_env() { + validate_env_present HOSTNAME true + validate_env_present API_PATH true + validate_env_present CERTIFICATE_TEMPLATE true + validate_env_present CERTIFICATE_AUTHORITY_LOGICAL_NAME true + validate_env_present OAUTH_TOKEN_URL true + validate_env_present OAUTH_CLIENT_ID true + validate_env_present OAUTH_CLIENT_SECRET true + + validate_env_present CERTIFICATE_AUTHORITY_HOSTNAME false +} + +ns_exists () { + local ns=$1 + if [ "$(kubectl get namespace -o json | jq --arg namespace "$ns" -e '.items[] | select(.metadata.name == $namespace) | .metadata.name')" ]; then + return 0 + fi + return 1 +} + +helm_exists () { + local namespace=$1 + local chart_name=$2 + if helm list -n "$namespace" | grep -q "$chart_name"; then + return 0 + fi + return 1 +} + +cr_exists () { + local fqtn=$1 + local ns=$2 + local name=$3 + if [ "$(kubectl -n "$ns" get "$fqtn" -o json | jq --arg name "$name" -e '.items[] | select(.metadata.name == $name) | .metadata.name')" ]; then + echo "$fqtn exists called $name in $ns" + return 0 + fi + return 1 +} + +secret_exists () { + local ns=$1 + local name=$2 + if [ "$(kubectl -n "$ns" get secret -o json | jq --arg name "$name" -e '.items[] | select(.metadata.name == $name) | .metadata.name')" ]; then + echo "secret exists called $name in $ns" + return 0 + fi + return 1 +} + +install_cert_manager() { + echo "📦 Installing cert-manager..." + + # Add jetstack repository if not already added + if ! helm repo list | grep -q jetstack; then + echo "Adding jetstack Helm repository..." + helm repo add jetstack https://charts.jetstack.io + fi + + helm repo update + + echo "Installing cert-manager version ${CERT_MANAGER_VERSION}..." + + helm install cert-manager jetstack/cert-manager \ + --namespace ${CERT_MANAGER_NAMESPACE} \ + --create-namespace \ + --version ${CERT_MANAGER_VERSION} \ + --set crds.enabled=true \ + --wait + + echo "✅ cert-manager installed successfully" +} + +install_cert_manager_issuer() { + echo "📦 Installing instance of $IMAGE_NAME with tag $IMAGE_TAG..." + + + if [[ "$IS_LOCAL_HELM" == "true" ]]; then + CHART_PATH=$CHART_PATH + + # Checking if chart path exists + if [ ! -d "$CHART_PATH" ]; then + echo "⚠️ Chart path not found at ${CHART_PATH}. Are you in the correct directory?" + exit 1 + fi + + VERSION_PARAM="" + else + CHART_PATH="command-issuer/command-cert-manager-issuer" + echo "Using Helm chart from repository for version ${HELM_CHART_VERSION}: $CHART_PATH..." + VERSION_PARAM="--version ${HELM_CHART_VERSION}" + fi + + # Only set the image repository parameter if we are deploying locally + if [[ "$IS_LOCAL_DEPLOYMENT" == "true" ]]; then + IMAGE_REPO_PARAM="--set image.repository=${IMAGE_REPO}/${IMAGE_NAME}" + else + IMAGE_REPO_PARAM="" + fi + + # Helm chart could be out of date for release candidates, so we will install from + # the chart defined in the repository. + helm install $IMAGE_NAME ${CHART_PATH} \ + --namespace ${MANAGER_NAMESPACE} \ + $VERSION_PARAM \ + $IMAGE_REPO_PARAM \ + --set "fullnameOverride=${IMAGE_NAME}" \ + --set image.tag=${IMAGE_TAG} \ + --set image.pullPolicy=Never \ + --wait + + echo "✅ $IMAGE_NAME installed successfully" +} + +create_issuer() { + echo "🔐 Creating issuer resources..." + + secretJson='{}' + secretJson=$(echo "$secretJson" | jq --arg version "v1" '.apiVersion = $version') + secretJson=$(echo "$secretJson" | jq --arg kind "Secret" '.kind = $kind') + secretJson=$(echo "$secretJson" | jq --arg name "$SIGNER_SECRET_NAME" '.metadata.name = $name') + + # OAuth credentials + secretJson=$(echo "$secretJson" | jq --arg type "Opaque" '.type = $type') + secretJson=$(echo "$secretJson" | jq --arg val "$OAUTH_TOKEN_URL" '.stringData.tokenUrl = $val') + secretJson=$(echo "$secretJson" | jq --arg val "$OAUTH_CLIENT_ID" '.stringData.clientId = $val') + secretJson=$(echo "$secretJson" | jq --arg val "$OAUTH_CLIENT_SECRET" '.stringData.clientSecret = $val') + secretJson=$(echo "$secretJson" | jq --arg val "$OAUTH_AUDIENCE" '.stringData.audience = $val') + secretJson=$(echo "$secretJson" | jq --arg val "$OAUTH_SCOPES" '.stringData.scopes = $val') + + echo "Creating secret called $SIGNER_SECRET_NAME in namespace $MANAGER_NAMESPACE" + if ! echo "$secretJson" | yq -P | kubectl -n "$MANAGER_NAMESPACE" apply -f -; then + echo "Failed to create $SIGNER_SECRET_NAME" + return 1 + fi + + kubectl -n "$ISSUER_NAMESPACE" apply -f - < /dev/null 2>&1 + + kubectl -n "$ISSUER_NAMESPACE" apply -f - < /dev/null; then + echo "Error: Minikube is not running. Please start it with 'minikube start'" + exit 1 +fi + +kubectl config use-context minikube +echo "Connected to Kubernetes context: $(kubectl config current-context)..." + +# 1. Connect to minikube Docker env +echo "📡 Connecting to Minikube Docker environment..." +eval $(minikube docker-env) +echo "🚀 Starting deployment to Minikube..." + +# 2. Deploy cert-manager Helm chart if not exists +echo "🔐 Checking for cert-manager installation..." +if ! helm_exists $CERT_MANAGER_NAMESPACE cert-manager; then + install_cert_manager +else + echo "✅ cert-manager already installed" +fi + +# 3. Create command-cert-manager-issuer namespace if it doesn't exist +kubectl create namespace ${MANAGER_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - + +# 4. Build the command-cert-manager-issuer Docker image +# This step is only needed if the image tag is "local" +if "$IS_LOCAL_DEPLOYMENT" = "true"; then + echo "🐳 Building ${FULL_IMAGE_NAME} Docker image..." + docker build -t ${FULL_IMAGE_NAME} . + echo "✅ Docker image built successfully" + + echo "📦 Listing Docker images..." + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}" | head -11 +fi + +# 5. Deploy the command-cert-manager-issuer Helm chart if not exists +echo "🎛️ Checking for $IMAGE_NAME installation..." + +# Check if the helm release exists. If so, destroy it. This ensures our Helm chart is always up to date. +if helm_exists $MANAGER_NAMESPACE $IMAGE_NAME; then + echo "💣 Uninstalling $IMAGE_NAME..." + helm uninstall $IMAGE_NAME -n ${MANAGER_NAMESPACE} +fi + +install_cert_manager_issuer + +# Find the deployment name (assuming it follows a pattern) +DEPLOYMENT_NAME=$(kubectl get deployments -n ${MANAGER_NAMESPACE} -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "$IMAGE_NAME") + +if kubectl get deployment ${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} >/dev/null 2>&1; then + # Patch the deployment + kubectl patch deployment ${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} -p "{ + \"spec\": { + \"template\": { + \"spec\": { + \"containers\": [{ + \"name\": \"${IMAGE_NAME}\", + \"image\": \"${FULL_IMAGE_NAME}\", + \"imagePullPolicy\": \"Never\" + }] + } + } + } + }" + + # Rollout deployment changes and apply the patch + kubectl rollout restart deployment/${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} + kubectl rollout status deployment/${DEPLOYMENT_NAME} -n ${MANAGER_NAMESPACE} --timeout=300s + + + echo "✅ Deployment patched and rolled out successfully" +else + echo "⚠️ Deployment ${DEPLOYMENT_NAME} not found. The Helm chart might use a different naming convention." + echo "Available deployments in ${MANAGER_NAMESPACE}:" + kubectl get deployments -n ${MANAGER_NAMESPACE} +fi + +echo "" +echo "🎉 Deployment complete!" +echo "" + +# Delete stray CertificateRequest resources from previous runs +delete_certificate_request +echo "" + +# Deploy Issuer +echo "🔐 Deploying $ISSUER_NAMESPACE namespace if not exists..." +kubectl create namespace ${ISSUER_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - +regenerate_issuer +echo "✅ $ISSUER_NAMESPACE namespace is ready" +echo "" + + +echo "" +echo "✅ Resource deployment completed. Ready to start running tests!" +# ================= END: Resource Deployment ===================== +# +# +# +# +# +# +# +# +# ================= BEGIN: Test Execution ======================== +echo "🚀 Running E2E tests..." +echo "" + +echo "🧪💬 Test 1: A generated certificate request should be successfully issued by Issuer." +regenerate_issuer +regenerate_certificate_request Issuer +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 1 completed successfully." +echo "" + +echo "🧪💬 Test 2: Add EnrollmentPatternId to Issuer resource" +regenerate_issuer +delete_issuer_specification_field certificateTemplate +add_issuer_specification_field enrollmentPatternId $ENROLLMENT_PATTERN_ID +regenerate_certificate_request Issuer +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 2 completed successfully." +echo "" + +echo "🧪💬 Test 3: Add EnrollmentPatternName to Issuer resource" +regenerate_issuer +delete_issuer_specification_field certificateTemplate +add_issuer_specification_field enrollmentPatternName "$ENROLLMENT_PATTERN_NAME" +regenerate_certificate_request Issuer +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 3 completed successfully." +echo "" + +echo "🧪💬 Test 4: Annotate CertificateRequest with certificateTemplate" +regenerate_issuer +delete_issuer_specification_field certificateTemplate +add_issuer_specification_field certificateTemplate "SomeDefaultTemplate" # This is a placeholder, will be overridden by annotation +regenerate_certificate_request Issuer +annotate_certificate_request "command-issuer.keyfactor.com/certificateTemplate" "$CERTIFICATE_TEMPLATE" +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 4 completed successfully." +echo "" + +echo "🧪💬 Test 5: Annotate CertificateRequest with enrollmentPatternId" +regenerate_issuer +delete_issuer_specification_field certificateTemplate +add_issuer_specification_field enrollmentPatternId 12345678 # This is a placeholder, will be overridden by annotation +regenerate_certificate_request Issuer +annotate_certificate_request "command-issuer.keyfactor.com/enrollmentPatternId" "$ENROLLMENT_PATTERN_ID" +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 5 completed successfully." +echo "" + +echo "🧪💬 Test 6: Annotate CertificateRequest with enrollmentPatternName" +regenerate_issuer +delete_issuer_specification_field certificateTemplate +add_issuer_specification_field enrollmentPatternName "SomeDefaultPattern" # This is a placeholder, will be overridden by annotation +regenerate_certificate_request Issuer +annotate_certificate_request "command-issuer.keyfactor.com/enrollmentPatternName" "$ENROLLMENT_PATTERN_NAME" +approve_certificate_request +check_certificate_request_status +echo "🧪✅ Test 6 completed successfully." +echo "" + +echo "🎉🎉🎉 Tests have completed successfully!" + +# ================= END: Test Execution ======================== \ No newline at end of file diff --git a/go.mod b/go.mod index 6cf8ced..5eec9a6 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/Keyfactor/command-cert-manager-issuer -go 1.23.4 +go 1.24 + +toolchain go1.24.0 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0 - github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12 + github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 + github.com/Keyfactor/keyfactor-go-client-sdk/v25 v25.0.2 github.com/cert-manager/cert-manager v1.16.2 github.com/go-logr/logr v1.4.2 github.com/stretchr/testify v1.10.0 - golang.org/x/oauth2 v0.26.0 + golang.org/x/oauth2 v0.30.0 k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.1 @@ -35,20 +37,19 @@ require ( require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.6 // indirect @@ -58,15 +59,13 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -74,8 +73,6 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -86,19 +83,17 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/spbsoluble/go-pkcs12 v0.3.3 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect - go.mozilla.org/pkcs7 v0.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.33.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/net v0.41.0 + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.10.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.223.0 diff --git a/go.sum b/go.sum index 2101b7b..d2d4df4 100644 --- a/go.sum +++ b/go.sum @@ -2,34 +2,30 @@ cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= -cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= -cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0 h1:z4TfQErC+YLPujwHPNeAkK2bl6O5hd7m1mve+qGh2Ko= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.2-rc.0/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12 h1:L/IXsbVR+cGW8ACQuA8a3nebux2sLQ4rpCGvFF4sIfg= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0-rc.12/go.mod h1:BiX76zEZTgRaUPDiRjnUWKtpEPQlSuko6XKBpBZxmX8= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0 h1:yMChWRnnxmcgLt6kEQ3FZfteps05v/qot5KXLXxa6so= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0/go.mod h1:HWb+S60YAALFVSfB8QuQ8ugjsjr+FHLQET0/4K7EVWw= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= +github.com/Keyfactor/keyfactor-go-client-sdk/v25 v25.0.2 h1:7VsZOYgMHAO2a1eeyVgDKel9TJXXYRQpd1EvSvp8lKA= +github.com/Keyfactor/keyfactor-go-client-sdk/v25 v25.0.2/go.mod h1:VnVW8x+pChhnOWBR1PNYPeCQQjlWIK1bwHI8i8j7UPI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -53,9 +49,6 @@ github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lSh github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -84,8 +77,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -93,8 +86,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -112,13 +103,9 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= -github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -139,8 +126,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -153,17 +140,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -190,13 +166,11 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= -github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -207,7 +181,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -217,16 +190,20 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= -go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -242,10 +219,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -264,64 +239,47 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= -golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -330,8 +288,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -340,13 +298,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/api v0.223.0 h1:JUTaWEriXmEy5AhvdMgksGGPEFsYfUKaPEYXd4c3Wvc= google.golang.org/api v0.223.0/go.mod h1:C+RS7Z+dDwds2b+zoAk5hN/eSfsiCn0UDrYof/M4d2M= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/command/client.go b/internal/command/client.go index f28ea60..e6d1ca0 100644 --- a/internal/command/client.go +++ b/internal/command/client.go @@ -1,5 +1,5 @@ /* -Copyright © 2024 Keyfactor +Copyright © 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,11 +18,15 @@ package command import ( "fmt" + "net/http" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + v1 "github.com/Keyfactor/keyfactor-go-client-sdk/v25/api/keyfactor/v1" + "github.com/go-logr/logr" + "github.com/golang-jwt/jwt/v5" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -43,8 +47,9 @@ func setAmbientTokenCredentialSource(source TokenCredentialSource) { } type Client interface { - EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) - GetAllMetadataFields() ([]commandsdk.MetadataField, error) + EnrollCSR(v1.ApiCreateEnrollmentCSRRequest) (*v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse, *http.Response, error) + GetAllMetadataFields(v1.ApiGetMetadataFieldsRequest) ([]v1.CSSCMSDataModelModelsMetadataType, *http.Response, error) + GetEnrollmentPatterns(v1.ApiGetEnrollmentPatternsRequest) ([]v1.EnrollmentPatternsEnrollmentPatternResponse, *http.Response, error) TestConnection() error } @@ -53,19 +58,24 @@ var ( ) type clientAdapter struct { - enrollCSR func(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) - getAllMetadataFields func() ([]commandsdk.MetadataField, error) - testConnection func() error + enrollCSR func(r v1.ApiCreateEnrollmentCSRRequest) (*v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse, *http.Response, error) + getAllMetadataFields func(r v1.ApiGetMetadataFieldsRequest) ([]v1.CSSCMSDataModelModelsMetadataType, *http.Response, error) + getEnrollmentPatterns func(r v1.ApiGetEnrollmentPatternsRequest) ([]v1.EnrollmentPatternsEnrollmentPatternResponse, *http.Response, error) + testConnection func() error } // EnrollCSR implements CertificateClient. -func (c *clientAdapter) EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) { - return c.enrollCSR(ea) +func (c *clientAdapter) EnrollCSR(r v1.ApiCreateEnrollmentCSRRequest) (*v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse, *http.Response, error) { + return c.enrollCSR(r) } // GetAllMetadataFields implements Client. -func (c *clientAdapter) GetAllMetadataFields() ([]commandsdk.MetadataField, error) { - return c.getAllMetadataFields() +func (c *clientAdapter) GetAllMetadataFields(r v1.ApiGetMetadataFieldsRequest) ([]v1.CSSCMSDataModelModelsMetadataType, *http.Response, error) { + return c.getAllMetadataFields(r) +} + +func (c *clientAdapter) GetEnrollmentPatterns(r v1.ApiGetEnrollmentPatternsRequest) ([]v1.EnrollmentPatternsEnrollmentPatternResponse, *http.Response, error) { + return c.getEnrollmentPatterns(r) } // TestConnection implements CertificateClient. @@ -95,6 +105,11 @@ type azure struct { // GetAccessToken implements TokenCredential. func (a *azure) GetAccessToken(ctx context.Context) (string, error) { + log := log.FromContext(ctx) + + // To prevent clogging logs every time JWT is generated + initializing := a.cred == nil + // Lazily create the credential if needed if a.cred == nil { c, err := azidentity.NewDefaultAzureCredential(nil) @@ -104,6 +119,8 @@ func (a *azure) GetAccessToken(ctx context.Context) (string, error) { a.cred = c } + log.Info(fmt.Sprintf("generating Default Azure Credentials with scopes %s", strings.Join(a.scopes, " "))) + // Request a token with the provided scopes token, err := a.cred.GetToken(ctx, policy.TokenRequestOptions{ Scopes: a.scopes, @@ -112,8 +129,20 @@ func (a *azure) GetAccessToken(ctx context.Context) (string, error) { return "", fmt.Errorf("%w: failed to fetch token: %w", errTokenFetchFailure, err) } - log.FromContext(ctx).Info("fetched token using Azure DefaultAzureCredential") - return token.Token, nil + tokenString := token.Token + + if initializing { + // Only want to output this once, don't want to output this every time the JWT is generated + + log.Info("==== BEGIN DEBUG: DefaultAzureCredential JWT ======") + + printClaims(log, tokenString, []string{"aud", "appid", "azp", "iss", "sub", "oid"}) + + log.Info("==== END DEBUG: DefaultAzureCredential JWT ======") + } + + log.Info("fetched token using Azure DefaultAzureCredential") + return tokenString, nil } func newAzureDefaultCredentialSource(ctx context.Context, scopes []string) (*azure, error) { @@ -142,17 +171,28 @@ type gcp struct { // GetAccessToken implements TokenCredential. func (g *gcp) GetAccessToken(ctx context.Context) (string, error) { - // Lazily create the TokenSource if it's nil. log := log.FromContext(ctx) + + // To prevent clogging logs every time JWT is generated + initializing := g.tokenSource == nil + + // Lazily create the TokenSource if it's nil. if g.tokenSource == nil { + log.Info(fmt.Sprintf("generating default Google credentials with scopes: %s", strings.Join(g.scopes, " "))) + credentials, err := google.FindDefaultCredentials(ctx, g.scopes...) if err != nil { return "", fmt.Errorf("%w: failed to find GCP ADC: %w", errTokenFetchFailure, err) } log.Info(fmt.Sprintf("generating a Google OIDC ID token...")) + // Default audience to "command" if not provided + aud := getValueOrDefault(g.audience, "command") + + log.Info(fmt.Sprintf("generating Google id token with audience %s", aud)) + // Use credentials to generate a JWT (requires a service account) - tokenSource, err := idtoken.NewTokenSource(ctx, getValueOrDefault(g.audience, "command"), idtoken.WithCredentialsJSON(credentials.JSON)) + tokenSource, err := idtoken.NewTokenSource(ctx, aud, idtoken.WithCredentialsJSON(credentials.JSON)) if err != nil { return "", fmt.Errorf("%w: failed to get GCP ID Token Source: %w", errTokenFetchFailure, err) } @@ -171,6 +211,14 @@ func (g *gcp) GetAccessToken(ctx context.Context) (string, error) { return "", fmt.Errorf("%w: failed to fetch token from GCP ADC token source: %w", errTokenFetchFailure, err) } + if initializing { + // Only want to output this once, don't want to output this every time the JWT is generated + + log.Info("==== BEGIN DEBUG: Default Google ID Token JWT ======") + printClaims(log, token.AccessToken, []string{"aud", "iss", "sub", "email"}) + log.Info("==== END DEBUG: Default Google ID Token JWT ======") + } + log.Info("fetched token using GCP ApplicationDefaultCredential") return token.AccessToken, nil @@ -188,3 +236,26 @@ func newGCPDefaultCredentialSource(ctx context.Context, audience string, scopes tokenCredentialSource = source return source, nil } + +func printClaims(log logr.Logger, token string, claimsToPrint []string) error { + tokenRaw, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) + if err != nil { + log.Error(err, "failed to parse JWT") + return fmt.Errorf("failed to parse JWT: %w", err) + } + + claims, _ := tokenRaw.Claims.(jwt.MapClaims) + + // To assist with troubleshooting, only print access token claims relevant to Command configuration + for _, key := range claimsToPrint { + if value, ok := claims[key]; ok { + log.Info(fmt.Sprintf("\t%s: %s", key, value)) + } + } + + if issuer, err := claims.GetIssuer(); err != nil { + log.Info(fmt.Sprintf("\nNOTE: If you are receiving a HTTP 401 on requests to Command, make sure an identity provider in Command is configured with '%s' as the authority.\nThe discovery endpoint for your issuer can be found at %s/.well-known/openid-configuration.", issuer, issuer)) + } + + return nil +} diff --git a/internal/command/client_test.go b/internal/command/client_test.go new file mode 100644 index 0000000..80b3b78 --- /dev/null +++ b/internal/command/client_test.go @@ -0,0 +1,88 @@ +/* +Copyright © 2025 Keyfactor + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "testing" + + "github.com/go-logr/logr/testr" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" +) + +func TestPrintClaims(t *testing.T) { + testLogger := testr.New(t) + t.Run("valid jwt returns no error", func(t *testing.T) { + // Sample JWT with dummy claims (no signature needed for ParseUnverified) + claims := jwt.MapClaims{ + "aud": "api://1234", + "iss": "https://sts.windows.net/tenant-id/", + "sub": "user-id", + } + token := createUnsignedJWT(t, claims) + + // Call the function + err := printClaims(testLogger, token, []string{"aud", "iss", "sub"}) + assert.NoError(t, err) + }) + + t.Run("jwt with no issuer does not error", func(t *testing.T) { + // Sample JWT with dummy claims (no signature needed for ParseUnverified) + claims := jwt.MapClaims{ + "aud": "api://1234", + "sub": "user-id", + } + token := createUnsignedJWT(t, claims) + + // Call the function + err := printClaims(testLogger, token, []string{"aud", "iss", "sub"}) + assert.NoError(t, err) + }) + + t.Run("jwt with empty claims does not error", func(t *testing.T) { + // Sample JWT with dummy claims (no signature needed for ParseUnverified) + claims := jwt.MapClaims{} + token := createUnsignedJWT(t, claims) + + // Call the function + err := printClaims(testLogger, token, []string{"aud", "iss", "sub"}) + assert.NoError(t, err) + }) + + t.Run("invalid jwt returns an error", func(t *testing.T) { + // Call the function + err := printClaims(testLogger, "abcdefghijklmnop", []string{"aud", "iss", "sub"}) + assert.Error(t, err) + }) + + t.Run("jwt with empty payload returns error", func(t *testing.T) { + // Call the function + err := printClaims(testLogger, "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0..", []string{"aud", "iss", "sub"}) + assert.Error(t, err) + }) +} + +func createUnsignedJWT(t *testing.T, claims jwt.MapClaims) string { + t.Helper() + + token := jwt.NewWithClaims(jwt.SigningMethodNone, claims) + str, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType) + if err != nil { + t.Fatalf("failed to create test token: %v", err) + } + return str +} diff --git a/internal/command/command.go b/internal/command/command.go index ffb672f..c11c0dc 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -1,5 +1,5 @@ /* -Copyright © 2024 Keyfactor +Copyright © 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,12 +22,16 @@ import ( "encoding/pem" "errors" "fmt" + "io" + "strconv" "strings" "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" - commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + commandsdk "github.com/Keyfactor/keyfactor-go-client-sdk/v25" + v1 "github.com/Keyfactor/keyfactor-go-client-sdk/v25/api/keyfactor/v1" cmpki "github.com/cert-manager/cert-manager/pkg/util/pki" + "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -42,6 +46,7 @@ var ( errInvalidSignerConfig = errors.New("invalid signer config") errInvalidCSR = errors.New("csr is invalid") errCommandEnrollmentFailure = errors.New("command enrollment failure") + errEnrollmentPatternFailure = errors.New("enrollment pattern failure") errTokenFetchFailure = errors.New("couldn't fetch bearer token") errAmbientCredentialCreationFailure = errors.New("failed to obtain ambient credentials") ) @@ -68,7 +73,7 @@ type Signer interface { Sign(context.Context, []byte, *SignConfig) ([]byte, []byte, error) } -type newCommandClientFunc func(*auth_providers.Server, *context.Context) (*commandsdk.Client, error) +type newCommandClientFunc func(*auth_providers.Server) (*commandsdk.APIClient, error) type signer struct { client Client @@ -167,6 +172,8 @@ func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Serve nonAmbientCredentialsConfigured := false if config.BasicAuth != nil { + log.Info("Using basic auth credential source") + basicAuthConfig := auth_providers.NewBasicAuthAuthenticatorBuilder(). WithUsername(config.BasicAuth.Username). WithPassword(config.BasicAuth.Password) @@ -177,6 +184,8 @@ func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Serve } if config.OAuth != nil { + log.Info("Using OAuth credential source") + oauthConfig := auth_providers.NewOAuthAuthenticatorBuilder(). WithTokenUrl(config.OAuth.TokenURL). WithClientId(config.OAuth.ClientID). @@ -223,19 +232,12 @@ func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Serve return nil, err } - server = &auth_providers.Server{ - Host: config.Hostname, - APIPath: config.APIPath, - AccessToken: token, - AuthType: "oauth", - ClientID: "", - ClientSecret: "", - OAuthTokenUrl: "", - Scopes: nil, - Audience: "", - SkipTLSVerify: false, - CACertPath: "", - } + oauthConfig := auth_providers.NewOAuthAuthenticatorBuilder(). + WithAccessToken(token). + WithCaCertificatePath("") + oauthConfig.CommandAuthConfig = authConfig + + server = oauthConfig.GetServerConfig() } log.Info("Configuration was valid - Successfully generated server config", "authMethod", server.AuthType, "hostname", server.Host, "apiPath", server.APIPath) @@ -243,7 +245,9 @@ func newServerConfig(ctx context.Context, config *Config) (*auth_providers.Serve } type SignConfig struct { - CertificateTemplate string + CertificateTemplate string // Deprecated, use EnrollmentPatternName or EnrollmentPatternId instead + EnrollmentPatternId int32 + EnrollmentPatternName string CertificateAuthorityLogicalName string CertificateAuthorityHostname string Meta *K8sMetadata @@ -251,8 +255,8 @@ type SignConfig struct { } func (s *SignConfig) validate() error { - if s.CertificateTemplate == "" { - return errors.New("certificateTemplate is required") + if s.CertificateTemplate == "" && s.EnrollmentPatternName == "" && s.EnrollmentPatternId == 0 { + return errors.New("either certificateTemplate, enrollmentPatternName, or enrollmentPatternId must be specified") } if s.CertificateAuthorityLogicalName == "" { return errors.New("certificateAuthorityLogicalName is required") @@ -277,15 +281,16 @@ func newInternalSigner(ctx context.Context, config *Config, newClientFunc newCom return nil, err } - client, err := newClientFunc(serverConfig, &ctx) + client, err := newClientFunc(serverConfig) if err != nil { return nil, fmt.Errorf("failed to create new Command API client: %w", err) } adapter := &clientAdapter{ - enrollCSR: client.EnrollCSR, - getAllMetadataFields: client.GetAllMetadataFields, - testConnection: client.AuthClient.Authenticate, + enrollCSR: client.V1.EnrollmentApi.CreateEnrollmentCSRExecute, + getAllMetadataFields: client.V1.MetadataFieldApi.GetMetadataFieldsExecute, + getEnrollmentPatterns: client.V1.EnrollmentPatternApi.GetEnrollmentPatternsExecute, + testConnection: client.V1.AuthClient.Authenticate, } log.Info("Successfully generated Command client") @@ -295,11 +300,11 @@ func newInternalSigner(ctx context.Context, config *Config, newClientFunc newCom } func NewHealthChecker(ctx context.Context, config *Config) (HealthChecker, error) { - return newInternalSigner(ctx, config, commandsdk.NewKeyfactorClient) + return newInternalSigner(ctx, config, commandsdk.NewAPIClient) } func NewSignerBuilder(ctx context.Context, config *Config) (Signer, error) { - return newInternalSigner(ctx, config, commandsdk.NewKeyfactorClient) + return newInternalSigner(ctx, config, commandsdk.NewAPIClient) } // Check implements HealthChecker. @@ -313,7 +318,7 @@ func (s *signer) Check(ctx context.Context) error { // CommandSupportsMetadata implements HealthChecker. func (s *signer) CommandSupportsMetadata() (bool, error) { - existingFields, err := s.client.GetAllMetadataFields() + existingFields, _, err := s.client.GetAllMetadataFields(v1.ApiGetMetadataFieldsRequest{}) if err != nil { return false, fmt.Errorf("failed to fetch metadata fields from connected Command instance: %w", err) } @@ -331,13 +336,14 @@ func (s *signer) CommandSupportsMetadata() (bool, error) { // Create a lookup map (set) of existing field names existingFieldSet := make(map[string]struct{}, len(existingFields)) for _, field := range existingFields { - existingFieldSet[field.Name] = struct{}{} + name := field.Name.Get() + existingFieldSet[*name] = struct{}{} } // Check that every expected field is present for _, expectedField := range expectedFieldsSlice { if _, found := existingFieldSet[expectedField]; !found { - // As soon as one required field is missing, return false + // As soon as one recommended field is missing, return false return false, nil } } @@ -367,16 +373,31 @@ func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) // Override defaults from annotations if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateTemplate"]; exists { + k8sLog.Info(fmt.Sprintf("Using certificateTemplate %q from annotations", value)) config.CertificateTemplate = value } if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateAuthorityLogicalName"]; exists { + k8sLog.Info(fmt.Sprintf("Using certificateAuthorityLogicalName %q from annotations", value)) config.CertificateAuthorityLogicalName = value } if value, exists := config.Annotations["command-issuer.keyfactor.com/certificateAuthorityHostname"]; exists { + k8sLog.Info(fmt.Sprintf("Using certificateAuthorityHostname %q from annotations", value)) config.CertificateAuthorityHostname = value } + if value, exists := config.Annotations["command-issuer.keyfactor.com/enrollmentPatternId"]; exists { + k8sLog.Info(fmt.Sprintf("Using enrollmentPatternId %q from annotations", value)) + conv, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return nil, nil, fmt.Errorf("%w: failed to parse enrollmentPatternId from annotations: %s", errInvalidSignerConfig, err) + } + config.EnrollmentPatternId = int32(conv) + } + if value, exists := config.Annotations["command-issuer.keyfactor.com/enrollmentPatternName"]; exists { + k8sLog.Info(fmt.Sprintf("Using enrollmentPatternName %q from annotations", value)) + config.EnrollmentPatternName = value + } - k8sLog.Info(fmt.Sprintf("Using certificate template %q and certificate authority %q (%s)", config.CertificateTemplate, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname)) + k8sLog.Info(fmt.Sprintf("Using certificate template %q and certificate authority %q (%s) and enrollment pattern ID %d and enrollment pattern name %s", config.CertificateTemplate, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname, config.EnrollmentPatternId, config.EnrollmentPatternName)) csr, err := parseCSR(csrBytes) if err != nil { @@ -400,14 +421,40 @@ func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) k8sLog.Info(fmt.Sprintf("URI SAN: %s", uri.String())) } - modelRequest := commandsdk.EnrollCSRFctArgs{ - CSR: string(csrBytes), - Template: config.CertificateTemplate, - CertFormat: enrollmentPEMFormat, - Timestamp: time.Now().Format(time.RFC3339), - IncludeChain: true, - SANs: &commandsdk.SANs{}, - Metadata: map[string]interface{}{}, + req := v1.ApiCreateEnrollmentCSRRequest{} + req = req.XCertificateformat(enrollmentPEMFormat) + + var template *string = nil + var enrollmentPatternId *int32 = nil + + // Populate certificate template if defined + if config.CertificateTemplate != "" { + k8sLog.Info(fmt.Sprintf("Using certificate template from config. Name: %s", config.CertificateTemplate)) + template = &config.CertificateTemplate + } + + // Populate enrollment pattern ID or name if defined + if config.EnrollmentPatternId != 0 { + k8sLog.Info(fmt.Sprintf("Using enrollment pattern ID from config. ID: %d", config.EnrollmentPatternId)) + enrollmentPatternId = &config.EnrollmentPatternId + } else if config.EnrollmentPatternName != "" { + pattern, err := getEnrollmentPatternByName(ctx, k8sLog, s, config.EnrollmentPatternName) + if err != nil { + return nil, nil, err + } + + enrollmentPatternId = pattern.Id + k8sLog.Info(fmt.Sprintf("Using enrollment pattern ID: %d", *enrollmentPatternId)) + } + + modelRequest := v1.EnrollmentCSREnrollmentRequest{ + CSR: string(csrBytes), + EnrollmentPatternId: *v1.NewNullableInt32(enrollmentPatternId), + Template: *v1.NewNullableString(template), + Timestamp: ptr(time.Now()), + IncludeChain: ptr(true), + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, } if config.Meta != nil { @@ -431,11 +478,29 @@ func (s *signer) Sign(ctx context.Context, csrBytes []byte, config *SignConfig) caBuilder.WriteString("\\") } caBuilder.WriteString(config.CertificateAuthorityLogicalName) - modelRequest.CertificateAuthority = caBuilder.String() + modelRequest.CertificateAuthority = *v1.NewNullableString(ptr(caBuilder.String())) - commandCsrResponseObject, err := s.client.EnrollCSR(&modelRequest) + req = req.EnrollmentCSREnrollmentRequest(modelRequest) + + // Avoid nil pointer dereference in logs + loggedEnrollmentPatternId := int32(0) + if enrollmentPatternId != nil { + loggedEnrollmentPatternId = *enrollmentPatternId + } + + k8sLog.Info(fmt.Sprintf("Enrolling certificate with Command using template %q and CA %q and enrollment pattern ID %d", config.CertificateTemplate, caBuilder.String(), loggedEnrollmentPatternId)) + + commandCsrResponseObject, _, err := s.client.EnrollCSR(req) if err != nil { - 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", config.CertificateTemplate, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) + detail := fmt.Sprintf("error enrolling certificate with Command. Verify that the certificate authority %q (%s) is configured correctly", config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) + + if template != nil { + detail += fmt.Sprintf(" and that the certificate template %q exists in Command", *template) + } + + if enrollmentPatternId != nil { + detail += fmt.Sprintf(". Make sure enrollment pattern ID %d is configured to use certificate authority %q (%s) and the security role is configured to use this enrollment pattern.", *enrollmentPatternId, config.CertificateAuthorityLogicalName, config.CertificateAuthorityHostname) + } if len(extractMetadataFromAnnotations(config.Annotations)) > 0 { detail += ". Also verify that the metadata fields provided exist in Command" @@ -495,3 +560,41 @@ func parseCSR(pemBytes []byte) (*x509.CertificateRequest, error) { func ptr[T any](v T) *T { return &v } + +// getEnrollmentPatternByName retrieves an enrollment pattern by its name from Command. +func getEnrollmentPatternByName(ctx context.Context, log logr.Logger, s *signer, enrollmentPatternName string) (*v1.EnrollmentPatternsEnrollmentPatternResponse, error) { + log.Info(fmt.Sprintf("Looking up enrollment pattern %q in Command...", enrollmentPatternName)) + + var model *v1.EnrollmentPatternsEnrollmentPatternResponse + + queryString := fmt.Sprintf("Name -eq \"%s\"", enrollmentPatternName) + patterns, httpResp, err := s.client.GetEnrollmentPatterns(v1.ApiGetEnrollmentPatternsRequest{}.QueryString(queryString)) + + if err != nil { + // Capture the error message which should indicate the failure reason + msg := "" + if httpResp != nil && httpResp.Body != nil { + defer httpResp.Body.Close() + bodyBytes, _ := io.ReadAll(httpResp.Body) + msg += string(bodyBytes) + } + detail := fmt.Sprintf("error fetching enrollment patterns from Command: %s. Details: %s", err, msg) + return nil, fmt.Errorf("%w: %s: %w", errEnrollmentPatternFailure, detail, err) + } + + if len(patterns) == 0 { + detail := fmt.Sprintf("enrollment pattern not found: %s", enrollmentPatternName) + return nil, fmt.Errorf("%w: %s", errEnrollmentPatternFailure, detail) + } + + if len(patterns) > 1 { + detail := fmt.Sprintf("multiple enrollment patterns found: %s", enrollmentPatternName) + return nil, fmt.Errorf("%w: %s", errEnrollmentPatternFailure, detail) + } + + model = &patterns[0] + + log.Info(fmt.Sprintf("Enrollment pattern %s found in Command", enrollmentPatternName)) + + return model, nil +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 27f8e64..e73ac5f 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1,5 +1,5 @@ /* -Copyright © 2024 Keyfactor +Copyright © 2025 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ import ( "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" - commandsdk "github.com/Keyfactor/keyfactor-go-client/v3/api" + v1 "github.com/Keyfactor/keyfactor-go-client-sdk/v25/api/keyfactor/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -200,9 +200,9 @@ func TestSignConfigValidate(t *testing.T) { wantErr string }{ { - name: "missing certificateTemplate", - config: &SignConfig{CertificateTemplate: "", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, - wantErr: "certificateTemplate is required", + name: "missing certificateTemplate and enrollmentPatternName and enrollmentPatternId", + config: &SignConfig{CertificateTemplate: "", EnrollmentPatternName: "", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "either certificateTemplate, enrollmentPatternName, or enrollmentPatternId must be specified", }, { name: "missing certificateAuthorityLogicalName", @@ -210,10 +210,25 @@ func TestSignConfigValidate(t *testing.T) { wantErr: "certificateAuthorityLogicalName is required", }, { - name: "all valid fields", + name: "all valid fields (both certificateTemplate and enrollmentPatternName specified)", + config: &SignConfig{CertificateTemplate: "myTemplate", EnrollmentPatternName: "My Enrollment Pattern", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "", + }, + { + name: "all valid fields (only certificateTemplate specified)", config: &SignConfig{CertificateTemplate: "myTemplate", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, wantErr: "", }, + { + name: "all valid fields (only enrollmentPatternName specified)", + config: &SignConfig{EnrollmentPatternName: "My Enrollment Pattern", CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "", + }, + { + name: "all valid fields (only enrollmentPatternId specified)", + config: &SignConfig{EnrollmentPatternId: 123, CertificateAuthorityLogicalName: "ca-logical", CertificateAuthorityHostname: "ca.example.com"}, + wantErr: "", + }, { name: "valid with optional fields", config: &SignConfig{ @@ -242,10 +257,6 @@ func TestSignConfigValidate(t *testing.T) { } } -var ( - _ commandsdk.AuthConfig = &fakeCommandAuthenticator{} -) - type fakeCommandAuthenticator struct { client *http.Client config *auth_providers.Server @@ -266,19 +277,6 @@ func (f *fakeCommandAuthenticator) GetServerConfig() *auth_providers.Server { return f.config } -func newFakeCommandClientFunc(httpClient *http.Client) newCommandClientFunc { - return newCommandClientFunc(func(s *auth_providers.Server, ctx *context.Context) (*commandsdk.Client, error) { - client := &commandsdk.Client{ - AuthClient: &fakeCommandAuthenticator{ - client: httpClient, - config: s, - }, - } - - return client, nil - }) -} - func TestNewServerConfig(t *testing.T) { testCases := map[string]struct { @@ -365,25 +363,31 @@ var ( ) type fakeClient struct { - enrollCallback func(*commandsdk.EnrollCSRFctArgs) - enrollResponse *commandsdk.EnrollResponse + enrollCallback func(v1.ApiCreateEnrollmentCSRRequest) + enrollResponse *v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse - metadataFields []commandsdk.MetadataField + metadataFields []v1.CSSCMSDataModelModelsMetadataType + enrollmentPatterns []v1.EnrollmentPatternsEnrollmentPatternResponse err error } // EnrollCSR implements Client. -func (f *fakeClient) EnrollCSR(ea *commandsdk.EnrollCSRFctArgs) (*commandsdk.EnrollResponse, error) { +func (f *fakeClient) EnrollCSR(r v1.ApiCreateEnrollmentCSRRequest) (*v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse, *http.Response, error) { if f.enrollCallback != nil { - f.enrollCallback(ea) + f.enrollCallback(r) } - return f.enrollResponse, f.err + return f.enrollResponse, nil, f.err } // GetAllMetadataFields implements Client. -func (f *fakeClient) GetAllMetadataFields() ([]commandsdk.MetadataField, error) { - return f.metadataFields, f.err +func (f *fakeClient) GetAllMetadataFields(v1.ApiGetMetadataFieldsRequest) ([]v1.CSSCMSDataModelModelsMetadataType, *http.Response, error) { + return f.metadataFields, nil, f.err +} + +// GetEnrollmentPatterns implements Client. +func (f *fakeClient) GetEnrollmentPatterns(v1.ApiGetEnrollmentPatternsRequest) ([]v1.EnrollmentPatternsEnrollmentPatternResponse, *http.Response, error) { + return f.enrollmentPatterns, nil, f.err } // TestConnection implements Client. @@ -391,6 +395,14 @@ func (f *fakeClient) TestConnection() error { return f.err } +type EnrollmentCSRRequest struct { + EnrollmentPatternId int32 + Template string + CertificateAuthority string + SANs map[string][]string + Metadata map[string]interface{} +} + func TestSign(t *testing.T) { caCert, rootKey := issueTestCertificate(t, "Root-CA", nil, nil) caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}) @@ -403,21 +415,23 @@ func TestSign(t *testing.T) { expectedLeafAndChain := append([]*x509.Certificate{leafCert}, issuingCert) + enrollmentPatternName := "fake-enrollment-pattern" certificateTemplateName := "fake-cert-template" certificateAuthorityLogicalName := "fake-issuing-ca" certificateAuthorityHostname := "pki.example.com" testCases := map[string]struct { enrollCSRFunctionError error + enrollmentPatterns []v1.EnrollmentPatternsEnrollmentPatternResponse // Request config *SignConfig // Expected - expectedEnrollArgs *commandsdk.EnrollCSRFctArgs + expectedEnrollArgs *EnrollmentCSRRequest expectedSignError error }{ - "success-no-meta": { + "success-no-meta-certificate-template": { // Request config: &SignConfig{ CertificateTemplate: certificateTemplateName, @@ -428,17 +442,84 @@ func TestSign(t *testing.T) { }, // Expected - expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + expectedEnrollArgs: &EnrollmentCSRRequest{ Template: certificateTemplateName, CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), - SANs: &commandsdk.SANs{}, + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-no-meta-enrollment-pattern-id": { + // Request + config: &SignConfig{ + EnrollmentPatternId: 12345, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &EnrollmentCSRRequest{ + EnrollmentPatternId: 12345, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-no-meta-enrollment-pattern-name": { + enrollmentPatterns: []v1.EnrollmentPatternsEnrollmentPatternResponse{ + v1.EnrollmentPatternsEnrollmentPatternResponse{ + Id: ptr(int32(12345)), + Name: *v1.NewNullableString(&enrollmentPatternName), + }, + }, + + // Request + config: &SignConfig{ + EnrollmentPatternName: enrollmentPatternName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &EnrollmentCSRRequest{ + EnrollmentPatternId: 12345, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-no-meta-enrollment-pattern-id-overwrites-pattern-name": { + enrollmentPatterns: []v1.EnrollmentPatternsEnrollmentPatternResponse{}, // This would fail if enrollment pattern name was used + // Request + config: &SignConfig{ + EnrollmentPatternId: 12345, + EnrollmentPatternName: enrollmentPatternName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &EnrollmentCSRRequest{ + EnrollmentPatternId: 12345, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: map[string][]string{}, Metadata: map[string]interface{}{}, }, expectedSignError: nil, }, - "success-annotation-config-override": { + "success-annotation-config-override-pattern-id": { // Request config: &SignConfig{ + EnrollmentPatternId: 67890, CertificateTemplate: certificateTemplateName, CertificateAuthorityLogicalName: certificateAuthorityLogicalName, CertificateAuthorityHostname: certificateAuthorityHostname, @@ -447,14 +528,49 @@ func TestSign(t *testing.T) { "command-issuer.keyfactor.com/certificateTemplate": "template-override", "command-issuer.keyfactor.com/certificateAuthorityLogicalName": "logicalname-override", "command-issuer.keyfactor.com/certificateAuthorityHostname": "hostname-override", + "command-issuer.keyfactor.com/enrollmentPatternId": "12345", }, }, // Expected - expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + expectedEnrollArgs: &EnrollmentCSRRequest{ + EnrollmentPatternId: 12345, Template: "template-override", CertificateAuthority: fmt.Sprintf("%s\\%s", "hostname-override", "logicalname-override"), - SANs: &commandsdk.SANs{}, + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, + }, + expectedSignError: nil, + }, + "success-annotation-config-override-pattern-name": { + enrollmentPatterns: []v1.EnrollmentPatternsEnrollmentPatternResponse{ + v1.EnrollmentPatternsEnrollmentPatternResponse{ + Id: ptr(int32(12345)), + Name: *v1.NewNullableString(ptr("enrollment-pattern-override")), + }, + }, + + // Request + config: &SignConfig{ + EnrollmentPatternName: enrollmentPatternName, + CertificateTemplate: certificateTemplateName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: map[string]string{ + "command-issuer.keyfactor.com/certificateTemplate": "template-override", + "command-issuer.keyfactor.com/certificateAuthorityLogicalName": "logicalname-override", + "command-issuer.keyfactor.com/certificateAuthorityHostname": "hostname-override", + "command-issuer.keyfactor.com/enrollmentPatternName": "enrollment-pattern-override", + }, + }, + + // Expected + expectedEnrollArgs: &EnrollmentCSRRequest{ + EnrollmentPatternId: 12345, + Template: "template-override", + CertificateAuthority: fmt.Sprintf("%s\\%s", "hostname-override", "logicalname-override"), + SANs: map[string][]string{}, Metadata: map[string]interface{}{}, }, expectedSignError: nil, @@ -479,10 +595,10 @@ func TestSign(t *testing.T) { }, // Expected - expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + expectedEnrollArgs: &EnrollmentCSRRequest{ Template: certificateTemplateName, CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), - SANs: &commandsdk.SANs{}, + SANs: map[string][]string{}, Metadata: map[string]interface{}{ CommandMetaControllerNamespace: "namespace", CommandMetaControllerKind: "Issuer", @@ -508,10 +624,10 @@ func TestSign(t *testing.T) { }, // Expected - expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + expectedEnrollArgs: &EnrollmentCSRRequest{ Template: certificateTemplateName, CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), - SANs: &commandsdk.SANs{}, + SANs: map[string][]string{}, Metadata: map[string]interface{}{ "testMetadata": "test", }, @@ -530,30 +646,50 @@ func TestSign(t *testing.T) { }, // Expected - expectedEnrollArgs: &commandsdk.EnrollCSRFctArgs{ + expectedEnrollArgs: &EnrollmentCSRRequest{ Template: certificateTemplateName, CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), - SANs: &commandsdk.SANs{}, + SANs: map[string][]string{}, Metadata: map[string]interface{}{}, }, expectedSignError: errCommandEnrollmentFailure, }, + "enroll-csr-err-enrollment-pattern-not-found": { + enrollmentPatterns: []v1.EnrollmentPatternsEnrollmentPatternResponse{}, + + // Request + config: &SignConfig{ + EnrollmentPatternName: enrollmentPatternName, + CertificateAuthorityLogicalName: certificateAuthorityLogicalName, + CertificateAuthorityHostname: certificateAuthorityHostname, + Meta: nil, + Annotations: nil, + }, + + // Expected + expectedEnrollArgs: &EnrollmentCSRRequest{ + Template: certificateTemplateName, + CertificateAuthority: fmt.Sprintf("%s\\%s", certificateAuthorityHostname, certificateAuthorityLogicalName), + SANs: map[string][]string{}, + Metadata: map[string]interface{}{}, + }, + + expectedSignError: errEnrollmentPatternFailure, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - cb := func(ea *commandsdk.EnrollCSRFctArgs) { - require.Equal(t, tc.expectedEnrollArgs.CertificateAuthority, ea.CertificateAuthority) - require.Equal(t, tc.expectedEnrollArgs.Template, ea.Template) - - require.Equal(t, tc.expectedEnrollArgs.Metadata, ea.Metadata) + cb := func(req v1.ApiCreateEnrollmentCSRRequest) { + require.NotNil(t, req) } client := fakeClient{ err: tc.enrollCSRFunctionError, - enrollResponse: certificateRestResponseFromExpectedCerts(t, expectedLeafAndChain, []*x509.Certificate{caCert}), - enrollCallback: cb, + enrollResponse: certificateRestResponseFromExpectedCerts(t, expectedLeafAndChain, []*x509.Certificate{caCert}), + enrollmentPatterns: tc.enrollmentPatterns, + enrollCallback: cb, } signer := signer{ client: &client, @@ -578,39 +714,67 @@ func TestSign(t *testing.T) { func TestCommandSupportsMetadata(t *testing.T) { testCases := map[string]struct { - presentMeta []commandsdk.MetadataField + presentMeta []v1.CSSCMSDataModelModelsMetadataType // Expected expected bool }{ - "success-no-meta": { - presentMeta: []commandsdk.MetadataField{}, + "failure-no-meta": { + presentMeta: []v1.CSSCMSDataModelModelsMetadataType{}, + + // Expected + expected: false, + }, + "failure-missing-meta": { + presentMeta: []v1.CSSCMSDataModelModelsMetadataType{ + { + Name: *v1.NewNullableString(ptr(CommandMetaControllerNamespace)), + }, + { + Name: *v1.NewNullableString(ptr(CommandMetaControllerKind)), + }, + { + Name: *v1.NewNullableString(ptr(CommandMetaControllerResourceGroupName)), + }, + // { + // Name: CommandMetaIssuerName, + // }, + { + Name: *v1.NewNullableString(ptr(CommandMetaIssuerNamespace)), + }, + { + Name: *v1.NewNullableString(ptr(CommandMetaControllerReconcileId)), + }, + { + Name: *v1.NewNullableString(ptr(CommandMetaCertificateSigningRequestNamespace)), + }, + }, // Expected expected: false, }, "success-all-meta": { - presentMeta: []commandsdk.MetadataField{ + presentMeta: []v1.CSSCMSDataModelModelsMetadataType{ { - Name: CommandMetaControllerNamespace, + Name: *v1.NewNullableString(ptr(CommandMetaControllerNamespace)), }, { - Name: CommandMetaControllerKind, + Name: *v1.NewNullableString(ptr(CommandMetaControllerKind)), }, { - Name: CommandMetaControllerResourceGroupName, + Name: *v1.NewNullableString(ptr(CommandMetaControllerResourceGroupName)), }, { - Name: CommandMetaIssuerName, + Name: *v1.NewNullableString(ptr(CommandMetaIssuerName)), }, { - Name: CommandMetaIssuerNamespace, + Name: *v1.NewNullableString(ptr(CommandMetaIssuerNamespace)), }, { - Name: CommandMetaControllerReconcileId, + Name: *v1.NewNullableString(ptr(CommandMetaControllerReconcileId)), }, { - Name: CommandMetaCertificateSigningRequestNamespace, + Name: *v1.NewNullableString(ptr(CommandMetaCertificateSigningRequestNamespace)), }, }, @@ -641,10 +805,11 @@ func assertErrorIs(t *testing.T, expectedError, actualError error) { if !assert.Error(t, actualError) { return } + assert.Truef(t, errors.Is(actualError, expectedError), "unexpected error type. expected: %v, got: %v", expectedError, actualError) } -func certificateRestResponseFromExpectedCerts(t *testing.T, leafCertAndChain []*x509.Certificate, rootCAs []*x509.Certificate) *commandsdk.EnrollResponse { +func certificateRestResponseFromExpectedCerts(t *testing.T, leafCertAndChain []*x509.Certificate, rootCAs []*x509.Certificate) *v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse { require.NotEqual(t, 0, len(leafCertAndChain)) leaf := string(pem.EncodeToMemory(&pem.Block{Bytes: leafCertAndChain[0].Raw, Type: "CERTIFICATE"})) @@ -656,20 +821,20 @@ func certificateRestResponseFromExpectedCerts(t *testing.T, leafCertAndChain []* certs = append(certs, string(pem.EncodeToMemory(&pem.Block{Bytes: cert.Raw, Type: "CERTIFICATE"}))) } - response := &commandsdk.EnrollResponse{ - Certificates: certs, - CertificateInformation: commandsdk.CertificateInformation{ - SerialNumber: "", - IssuerDN: "", - Thumbprint: "", - KeyfactorID: 0, - KeyfactorRequestID: 0, - PKCS12Blob: "", - Certificates: certs, - RequestDisposition: "", - DispositionMessage: "", - EnrollmentContext: nil, + response := &v1.CSSCMSDataModelModelsEnrollmentCSREnrollmentResponse{ + CertificateInformation: &v1.CSSCMSDataModelModelsPkcs10CertificateResponse{ + SerialNumber: *v1.NewNullableString(ptr("")), + IssuerDN: *v1.NewNullableString(ptr("")), + Thumbprint: *v1.NewNullableString(ptr("")), + KeyfactorID: ptr(int32(0)), + Certificates: certs, + WorkflowInstanceId: nil, + RequestDisposition: *v1.NewNullableString(ptr("")), + DispositionMessage: *v1.NewNullableString(ptr("")), + EnrollmentContext: nil, + WorkflowReferenceId: nil, }, + Metadata: map[string]string{}, } return response } diff --git a/internal/controller/certificaterequest_controller.go b/internal/controller/certificaterequest_controller.go index 2f2ba57..6da4814 100644 --- a/internal/controller/certificaterequest_controller.go +++ b/internal/controller/certificaterequest_controller.go @@ -233,6 +233,8 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R signConfig := &command.SignConfig{ CertificateTemplate: issuer.GetSpec().CertificateTemplate, + EnrollmentPatternId: issuer.GetSpec().EnrollmentPatternId, + EnrollmentPatternName: issuer.GetSpec().EnrollmentPatternName, CertificateAuthorityLogicalName: issuer.GetSpec().CertificateAuthorityLogicalName, CertificateAuthorityHostname: issuer.GetSpec().CertificateAuthorityHostname, Annotations: certificateRequest.GetAnnotations(),