diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fa3ed22 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See GitHub's documentation for more information on this file: +# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/keyfactor-merge-store-types.yml b/.github/workflows/keyfactor-merge-store-types.yml deleted file mode 100644 index c70659f..0000000 --- a/.github/workflows/keyfactor-merge-store-types.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Keyfactor Merge Cert Store Types -on: [workflow_dispatch] - -jobs: - get-manifest-properties: - runs-on: windows-latest - outputs: - update_catalog: ${{ steps.read-json.outputs.update_catalog }} - integration_type: ${{ steps.read-json.outputs.integration_type }} - steps: - - uses: actions/checkout@v3 - - name: Store json - id: read-json - shell: pwsh - run: | - $json = Get-Content integration-manifest.json | ConvertFrom-Json - $myvar = $json.update_catalog - echo "update_catalog=$myvar" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - $myvar = $json.integration_type - echo "integration_type=$myvar" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - - call-update-store-types-workflow: - needs: get-manifest-properties - if: needs.get-manifest-properties.outputs.integration_type == 'orchestrator' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - uses: Keyfactor/actions/.github/workflows/update-store-types.yml@main - secrets: - token: ${{ secrets.UPDATE_STORE_TYPES }} diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 4a52481..6d8de53 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -1,28 +1,19 @@ -name: Starter Workflow -on: [workflow_dispatch, push, pull_request] +name: Keyfactor Bootstrap Workflow -jobs: - call-create-github-release-workflow: - uses: Keyfactor/actions/.github/workflows/github-release.yml@main - - call-dotnet-build-and-release-workflow: - needs: [call-create-github-release-workflow] - uses: Keyfactor/actions/.github/workflows/dotnet-build-and-release.yml@main - with: - release_version: ${{ needs.call-create-github-release-workflow.outputs.release_version }} - release_url: ${{ needs.call-create-github-release-workflow.outputs.release_url }} - release_dir: kubernetes-orchestrator-extension\bin\Release - secrets: - token: ${{ secrets.PRIVATE_PACKAGE_ACCESS }} +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' - call-generate-readme-workflow: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - uses: Keyfactor/actions/.github/workflows/generate-readme.yml@main - secrets: - token: ${{ secrets.APPROVE_README_PUSH }} - - call-update-catalog-workflow: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - uses: Keyfactor/actions/.github/workflows/update-catalog.yml@main +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@v2 secrets: - token: ${{ secrets.SDK_SYNC_PAT }} \ No newline at end of file + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} diff --git a/CHANGELOG.md b/CHANGELOG.md index b884f88..56567c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.1.2 + +## Bug Fixes +- fix(management): Management jobs for `K8STLSSecret` and `K8SSecret` types handle ECC keys. +- fix(pam): Fixed issue with 'remote' PAM not being able to execute unless the input was empty, making 'remote' PAM impossible. + # 1.1.1 ## Features diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..08385e2 --- /dev/null +++ b/Makefile @@ -0,0 +1,115 @@ +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +.PHONY: all +all: build + +# Required environemnt variables for the project +ENV_VARS := AZURE_TENANT_ID AZURE_CLIENT_SECRET AZURE_CLIENT_ID AZURE_APP_GATEWAY_RESOURCE_ID + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.PHONY: reset +reset: ## Reset the environment + @echo "Resetting..." + @rm -rf test.env + @rm -rf .env + +.PHONY: setup +setup: ## Setup the environment for development + @if [ ! -f .test.env ]; then \ + echo "Creating .test.env file..."; \ + > .env; \ + for var in $(ENV_VARS); do \ + echo -n "Enter value for $$var: "; \ + read value; \ + echo "export $$var=$$value" >> .test.env; \ + done; \ + echo ".test.env file created with input values."; \ + fi + @if [ ! -f .env ]; then \ + echo "PROJECT_ROOT=$$(pwd)" >> .env; \ + echo "Select a project to target:"; \ + PS3="Enter your choice: "; \ + select opt in $$(ls */*.csproj); do \ + if [ -n "$$opt" ]; then \ + echo "You have selected $$opt"; \ + echo "PROJECT_FILE=$$opt" >> .env; \ + break; \ + else \ + echo "Invalid selection. Please try again."; \ + fi; \ + done; \ + echo "PROJECT_NAME=$$(basename $$(dirname $$(grep PROJECT_FILE .env | cut -d '=' -f 2)))" >> .env; \ + fi + +.PHONY: newtest +newtest: setup ## Create a new test project + @source .env; \ + testProjectName="$$PROJECT_NAME".Tests; \ + echo "Creating new xUnit project called $$testProjectName"; \ + dotnet new xunit -o $$testProjectName; \ + dotnet sln add $$testProjectName/$$testProjectName.csproj; \ + dotnet add $$testProjectName reference $$PROJECT_FILE; + +.PHONY: installpackage +installpackage: ## Install a package to the project + @source .env; \ + echo "Select a project to install the package into"; \ + PS3="Selection: "; \ + select opt in $$(ls */*.csproj); do \ + if [ -n "$$opt" ]; then \ + echo "You have selected $$opt"; \ + break; \ + else \ + echo "Invalid selection. Please try again."; \ + fi; \ + done; \ + echo "Enter the package name to install: "; \ + read packageName; \ + echo "Installing $$packageName to $$opt"; \ + dotnet add $$opt package $$packageName; + +.PHONY: testall +testall: ## Run all tests. + @source .env; \ + source .test.env; \ + dotnet test + +.PHONY: test +test: ## Run a single test. + @source .env; \ + source .test.env; \ + dotnet test --no-restore --list-tests | \ + grep -A 1000 "The following Tests are available:" | \ + awk 'NR>1' | \ + cut -d ' ' -f 5- | \ + sed 's/(.*//i' | \ + sort | uniq | \ + fzf | \ + xargs -I {} dotnet test --filter {} --logger "console;verbosity=detailed" + +##@ Build + +.PHONY: build +build: ## Build the test project + dotnet build diff --git a/README.md b/README.md index 924d223..604a8d4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ + # Kubernetes Orchestrator Extension -The Kubernetes Orchestrator allows for the remote management of certificate stores defined in a Kubernetes cluster. The following types of Kubernetes resources are supported: -- Secrets - Kubernetes secrets of type `kubernetes.io/tls` or `Opaque` -- Certificates - Kubernetes certificates of type `certificates.k8s.io/v1` +The Kubernetes Orchestrator allows for the remote management of certificate stores defined in a Kubernetes cluster. The following types of Kubernetes resources are supported: kubernetes secrets of `kubernetes.io/tls` or `Opaque` and kubernetes certificates `certificates.k8s.io/v1` #### Integration status: Production - Ready for use in production environments. - ## About the Keyfactor Universal Orchestrator Extension This repository contains a Universal Orchestrator Extension which is a plugin to the Keyfactor Universal Orchestrator. Within the Keyfactor Platform, Orchestrators are used to manage “certificate stores” — collections of certificates and roots of trust that are found within and used by various applications. @@ -15,26 +13,22 @@ The Universal Orchestrator is part of the Keyfactor software distribution and is The Universal Orchestrator is the successor to the Windows Orchestrator. This Orchestrator Extension plugin only works with the Universal Orchestrator and does not work with the Windows Orchestrator. - - - ## Support for Kubernetes Orchestrator Extension -Kubernetes Orchestrator Extension is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. +Kubernetes Orchestrator Extension is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com ###### To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. +--- --- - ## Keyfactor Version Supported -The minimum version of the Keyfactor Universal Orchestrator Framework needed to run this version of the extension is 10.1 - +The minimum version of the Keyfactor Universal Orchestrator Framework needed to run this version of the extension is 10.x ## Platform Specific Notes The Keyfactor Universal Orchestrator may be installed on either Windows or Linux based platforms. The certificate operations supported by a capability may vary based what platform the capability is installed on. The table below indicates what capabilities are supported based on which platform the encompassing Universal Orchestrator is running. @@ -44,7 +38,7 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux |Supports Management Remove|✓ |✓ | |Supports Create Store|✓ |✓ | |Supports Discovery|✓ |✓ | -|Supports Renrollment| | | +|Supports Reenrollment| | | |Supports Inventory|✓ |✓ | @@ -476,9 +470,9 @@ kfutil store-types create --name K8SSecret ##### UI Custom Fields Tab | Name | Display Name | Type | Required | Default Value | Description | |------------------|---------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | `default` | The K8S namespace the `Opaque` secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | ✓ | | The name of the K8S `Opaque` secret. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `secret` | | +| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `Opaque` secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `Opaque` secret. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `secret` | This is required and must be `secret`. | | IncludeCertChain | Include Certificate Chain | Bool | | `true` | Will default to `true` if not set. If set to `false` only leaf cert will be deployed. | | SeparateChain | SeparateChain | Bool | | `false` | Will default to `false` if not set. `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | @@ -526,13 +520,13 @@ kfutil store-types create --name K8STLSSecr ![k8sstlssecr_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8sstlssecr_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|------------------|----------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `tls` secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `tls` secret. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `tls_secret` | | -| IncludeCertChain | Include Certificate Chain | Bool | | `true` | If set to `false` only leaf cert will be deployed. | -| SeparateChain | SeparateChain | Bool | | `true` | `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | +| Name | Display Name | Type | Required | Default Value | Description | +|------------------|----------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `tls` secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `tls` secret. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `tls_secret` | This is required and must be `tls_secret`. | +| IncludeCertChain | Include Certificate Chain | Bool | | `true` | If set to `false` only leaf cert will be deployed. | +| SeparateChain | SeparateChain | Bool | | `true` | `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | ![k8sstlssecr_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8sstlssecr_custom_fields.png) @@ -579,16 +573,16 @@ kfutil store-types create --name K8SPKCS12 ![k8spkcs12_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8spkcs12_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | K8S namespace the PKCS12 secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains PKCS12 data. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `pkcs12` | This must be set to `pkcs12`. | -| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.p12` | The K8S secret field name to source the PKCS12 data from. You can provide an extension `.p12` or `.pfx` for a secret with a key `example.p12` | -| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the PKCS12 password from a K8S secret this is the field it will look for the password in. | -| PasswordIsK8SSecret | Password Is K8S Secret | Bool | ✓ | `false` | If you want to use the PKCS12 secret or a separate secret specific in `KubeSecretPasswordPath` set this to `true` | -| StorePassword | Kube Secret Password | Secret | | | If you want to specify the PKCS12 password on the store in Command use this. | -| StorePasswordPath | Kube Secret Password Path | String | | | Source PKCS12 password from a separate K8S secret. Pattern: `namespace_name/secret_name` | +| Name | Display Name | Type | Required | Default Value | Description | +|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | K8S namespace the PKCS12 secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains PKCS12 data. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `pkcs12` | This is required and must be set to `pkcs12`. | +| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.p12` | The K8S secret field name to source the PKCS12 data from. You can provide an extension `.p12` or `.pfx` for a secret with a key `example.p12` | +| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the PKCS12 password from the PKCS12 K8S secret, or a separate K8S secret, this is the field it will look for the password in. | +| PasswordIsK8SSecret | Password Is K8S Secret | Bool | | `false` | If sourcing the PKCS12 password from the PKCS12 K8S secret, or a separate K8S secret specified in `StorePasswordPath` set this to `true` | +| StorePassword | Store Password | Secret | | | If sourcing the PKCS12 password from Command use this. *Note* this will take precedence over `PasswordIsK8SSecret` | +| StorePasswordPath | Kube Secret Password Path | String | | | If sourcing the PKCS12 password from a separate K8S secret. Pattern: `namespace_name/secret_name` | ![k8spkcs12_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8spkcs12_custom_fields.png) @@ -643,16 +637,16 @@ kfutil store-types create --name K8SJKS ![k8sjks_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8sjks_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|--------------------------|-----------------------------|--------|----------|---------------|--------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | K8S namespace the JKS secret lives. This will override any value inferred in the `Store Path`. | -| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains JKS data. This will override any value inferred in the `Store Path`. | -| KubeSecretType | Kube Secret Type | String | ✓ | `jks` | | -| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.jks` | The K8S secret field name to source the JKS data from | -| PasswordFieldName | Password Field Name | String | ✓ | `password` | If sourcing the JKS password from a K8S secret this is the field it will look for the password in. | -| PasswordIsK8SSecret | Password Is K8S Secret | Bool | ✓ | `false` | If you want to use the JKS secret or a separate secret specific in `` set this to `true` | -| StorePassword | Kube Secret Password | Secret | | | If you want to specify the JKS password on the store in Command use this. | -| StorePasswordPath | Kube Secret Password Path | String | | | Source JKS password from a separate K8S secret. Pattern: `namespace_name/secret_name` | +| Name | Display Name | Type | Required | Default Value | Description | +|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | K8S namespace the K8S secret containing the JKS data lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S secret that contains the JKS data. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `jks` | This is required and must be `jks`. | +| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.jks` | The K8S secret field name to source the JKS data from. | +| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the JKS password from a another field on the K8S JKS secret or the separate K8S secret, this is the secret field name it will look for the password in. | +| PasswordIsK8SSecret | Password Is K8S Secret | Bool | | `false` | If sourcing the JKS password from the K8S JKS secret or a separate K8S secret set this to `true` | +| StorePassword | Store Password | Secret | | | If sourcing the JKS password from Command use this. *Note* this will take precedence over `PasswordIsK8SSecret` | +| StorePasswordPath | Kube Secret Password Path | String | | | Source the JKS password from a separate K8S secret. Pattern: `namespace_name/secret_name`. *Note* the Orchestrator K8S service account must have read access to the secret. | ![k8sjks_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8sjks_custom_fields.png) @@ -815,7 +809,7 @@ kfutil store-types create --name K8SCert |--------------------|---------------------------|--------|----------|---------------|--------------------------------------------------------------------------------------------------------| | KubeNamespace | Kube Namespace | String | | | The K8S namespace the `cert` resource lives. This will override any value inferred in the `Store Path` | | KubeSecretName | Kube Secret Name | String | | | The K8S `cert` name. This will override any value inferred in the `Store Path`. | -| KubeSecretType | Kube Secret Type | String | ✓ | `cert` | | +| KubeSecretType | Kube Secret Type | String | ✓ | `cert` | This is required and must be `cert`. | ![k8scert_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8scert_custom_fields.png) @@ -926,3 +920,6 @@ Here's what this looks like in the UI: [Apache](https://apache.org/licenses/LICENSE-2.0) +When creating cert store type manually, that store property names and entry parameter names are case sensitive + + diff --git a/TestConsole/TestConsole.csproj b/TestConsole/TestConsole.csproj index 718969a..cf64fd0 100644 --- a/TestConsole/TestConsole.csproj +++ b/TestConsole/TestConsole.csproj @@ -11,7 +11,7 @@ - + diff --git a/TestConsole/tests.json b/TestConsole/tests.json new file mode 100644 index 0000000..e69de29 diff --git a/TestConsole/tests.yml b/TestConsole/tests.yml new file mode 100644 index 0000000..e69de29 diff --git a/dev_k8s_cluster/MODULE.MD b/dev_k8s_cluster/MODULE.MD new file mode 100644 index 0000000..dc2fc2b --- /dev/null +++ b/dev_k8s_cluster/MODULE.MD @@ -0,0 +1,37 @@ +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5 | +| [kubernetes](#requirement\_kubernetes) | >=2.30 | + +## Providers + +| Name | Version | +|------|---------| +| [kubernetes](#provider\_kubernetes) | 2.30.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_cluster_role_binding.example](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role_binding) | resource | +| [kubernetes_namespace.keyfactor_command](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_namespace.test](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_secret.admin_user_token](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account.admin_user](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [kubernetes_namespace.dashboard](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/namespace) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [admin\_user\_token](#output\_admin\_user\_token) | n/a | diff --git a/dev_k8s_cluster/Makefile b/dev_k8s_cluster/Makefile new file mode 100644 index 0000000..f67d9df --- /dev/null +++ b/dev_k8s_cluster/Makefile @@ -0,0 +1,26 @@ +.DEFAULT_GOAL := help + +##@ Utility +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +deps: ## Install deps for macos + @brew install pre-commit tflint terraform terraform-docs + +docs: ## Run terraform-docs to update module docs. + @terraform-docs markdown . > MODULE.MD + @terraform-docs markdown table --output-file README.md --output-mode inject . + +lint: ## Run tflint + @tflint + +validate: ## Run terraform validate + @terraform init --upgrade + @terraform validate + +precommit/add: ## Install pre-commit hook + @pre-commit install + +precommit/remove: ## Uninstall pre-commit hook + @pre-commit uninstall + diff --git a/dev_k8s_cluster/README.md b/dev_k8s_cluster/README.md new file mode 100644 index 0000000..85b8841 --- /dev/null +++ b/dev_k8s_cluster/README.md @@ -0,0 +1,73 @@ +# Docker Desktop Kubernetes Cluster +This is a quick guide on how to setup a Kubernetes cluster using Docker Desktop that can be used for development purposes, +and testing the Keyfactor Command Kubernetes Universal Orchestrator extension. + +## Prerequisites +- [Docker Desktop](https://www.docker.com/products/docker-desktop) +- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +- [helm](https://helm.sh/docs/intro/install/) +- [terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) + +## Kubernetes Setup +1. Enable Kubernetes in Docker Desktop + - Open Docker Desktop + - Click on the Docker icon in the system tray + - Click on `Settings` + - Click on `Kubernetes` + - Check the box for `Enable Kubernetes` + - Click `Apply & Restart` +2. Configure kubectl to use the Docker Desktop Kubernetes cluster + - Run the following command in a terminal + ```shell + kubectl config use-context docker-desktop + ``` +3. Run the `setup_dashboard.sh` script to install the Kubernetes dashboard + ```shell + ./setup_dashboard.sh + ``` +4. Run the terraform code to create the necessary resources + ```shell + terraform init + terraform apply + ``` +Now the cluster is ready to be used for development and testing purposes. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5 | +| [kubernetes](#requirement\_kubernetes) | >=2.30 | + +## Providers + +| Name | Version | +|------|---------| +| [kubernetes](#provider\_kubernetes) | 2.30.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_cluster_role_binding.example](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role_binding) | resource | +| [kubernetes_namespace.keyfactor_command](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_namespace.test](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_secret.admin_user_token](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account.admin_user](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [kubernetes_namespace.dashboard](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/namespace) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [admin\_user\_token](#output\_admin\_user\_token) | n/a | + \ No newline at end of file diff --git a/dev_k8s_cluster/keyfactor_command/.auto.tfvars b/dev_k8s_cluster/keyfactor_command/.auto.tfvars new file mode 100644 index 0000000..e3b3da1 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/.auto.tfvars @@ -0,0 +1,10 @@ +client_machine_name="container_uo-11-4" +kubeconfig_file="keyfactor-orchestrator-sa-context.json" + +kfc_ca_domain="DC-CA.Command.local" +kfc_ca_name="CommandCA" +webserver_template="2YearTestWebServer" + +kube_namespace="keyfactor-command" +kube_tlssecr_name="tls-deployment" +kube_cluster_name="docker-desktop" \ No newline at end of file diff --git a/dev_k8s_cluster/keyfactor_command/MODULE.MD b/dev_k8s_cluster/keyfactor_command/MODULE.MD new file mode 100644 index 0000000..3ecd3ca --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/MODULE.MD @@ -0,0 +1,44 @@ +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5 | +| [keyfactor](#requirement\_keyfactor) | >=2.1.11 | + +## Providers + +| Name | Version | +|------|---------| +| [keyfactor](#provider\_keyfactor) | 2.1.11 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [keyfactor_certificate.pfx_enrollment_01](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate) | resource | +| [keyfactor_certificate_deployment.k8stlssecr](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate_deployment) | resource | +| [keyfactor_certificate_store.tls_store](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate_store) | resource | +| [keyfactor_agent.k8s](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/data-sources/agent) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [client\_machine\_name](#input\_client\_machine\_name) | Name of the client machine name of the Keyfactor Command Universal Orchestrator to use. | `string` | n/a | yes | +| [default\_ca\_domain](#input\_default\_ca\_domain) | The default certificate authority domain to use in certificate generation | `string` | `"DC-CA.Command.local"` | no | +| [default\_cert\_ca](#input\_default\_cert\_ca) | The default certificate authority to use in certificate generation | `string` | `"CommandCA1"` | no | +| [kfc\_ca\_domain](#input\_kfc\_ca\_domain) | The default CA domain to use for the certificate | `string` | `"Keyfactor"` | no | +| [kfc\_ca\_name](#input\_kfc\_ca\_name) | The name of the certificate authority to use for the Keyfactor Command certificate enrollments. | `string` | `"CommandCA"` | no | +| [kube\_cluster\_name](#input\_kube\_cluster\_name) | The name of the Kubernetes cluster to use | `string` | `"dev-cluster"` | no | +| [kube\_namespace](#input\_kube\_namespace) | Kubernetes namespace to store the certificate in | `string` | `"default"` | no | +| [kube\_tlssecr\_name](#input\_kube\_tlssecr\_name) | The name of the Kubernetes TLS secret for the Keyfactor Command `k8s-orchestrator` extension to manage | `string` | `"kfc-k8stlssecr-deployment"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | Path to the kubeconfig file | `string` | `"~/.kube/config"` | no | +| [webserver\_template](#input\_webserver\_template) | The webserver template to use in certificate generation | `string` | `"2YearTestWebServer"` | no | + +## Outputs + +No outputs. diff --git a/dev_k8s_cluster/keyfactor_command/Makefile b/dev_k8s_cluster/keyfactor_command/Makefile new file mode 100644 index 0000000..f67d9df --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/Makefile @@ -0,0 +1,26 @@ +.DEFAULT_GOAL := help + +##@ Utility +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +deps: ## Install deps for macos + @brew install pre-commit tflint terraform terraform-docs + +docs: ## Run terraform-docs to update module docs. + @terraform-docs markdown . > MODULE.MD + @terraform-docs markdown table --output-file README.md --output-mode inject . + +lint: ## Run tflint + @tflint + +validate: ## Run terraform validate + @terraform init --upgrade + @terraform validate + +precommit/add: ## Install pre-commit hook + @pre-commit install + +precommit/remove: ## Uninstall pre-commit hook + @pre-commit uninstall + diff --git a/dev_k8s_cluster/keyfactor_command/README.md b/dev_k8s_cluster/keyfactor_command/README.md new file mode 100644 index 0000000..21ac259 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/README.md @@ -0,0 +1,64 @@ +# Keyfactor Command K8S Orchestrator Demo +This repo contains Terraform code to demonstrate the use of the Keyfactor Command K8S Orchestrator extension to manage +certificates in a Kubernetes cluster. + +## Prerequisites +- [Keyfactor Command](https://www.keyfactor.com/products/command/) 10.0 or later +- [Keyfactor Command Universal Orchestrator](https://software.keyfactor.com/Core-OnPrem/v10.5/Content/InstallingAgents/Introduction.htm) +- [Keyfactor Command K8S Universal Orchestrator Extension](https://github.com/Keyfactor/k8s-orchestrator?tab=readme-ov-file#kubernetes-orchestrator-extension-installation) +- [Kubernetes Cluster Credentials](https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes) +- [Terraform](https://www.terraform.io/downloads.html) 1.0 or later + +## Usage +1. Clone the repository +2. Update the `terraform.tfvars` file with the appropriate values +3. Run `terraform init` +4. Run `terraform apply` +5. Run `terraform destroy` to remove the resources + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5 | +| [keyfactor](#requirement\_keyfactor) | >=2.1.11 | + +## Providers + +| Name | Version | +|------|---------| +| [keyfactor](#provider\_keyfactor) | 2.1.11 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [keyfactor_certificate.pfx_enrollment_01](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate) | resource | +| [keyfactor_certificate_deployment.k8stlssecr](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate_deployment) | resource | +| [keyfactor_certificate_store.tls_store](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/resources/certificate_store) | resource | +| [keyfactor_agent.k8s](https://registry.terraform.io/providers/keyfactor-pub/keyfactor/latest/docs/data-sources/agent) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [client\_machine\_name](#input\_client\_machine\_name) | Name of the client machine name of the Keyfactor Command Universal Orchestrator to use. | `string` | n/a | yes | +| [default\_ca\_domain](#input\_default\_ca\_domain) | The default certificate authority domain to use in certificate generation | `string` | `"DC-CA.Command.local"` | no | +| [default\_cert\_ca](#input\_default\_cert\_ca) | The default certificate authority to use in certificate generation | `string` | `"CommandCA1"` | no | +| [kfc\_ca\_domain](#input\_kfc\_ca\_domain) | The default CA domain to use for the certificate | `string` | `"Keyfactor"` | no | +| [kfc\_ca\_name](#input\_kfc\_ca\_name) | The name of the certificate authority to use for the Keyfactor Command certificate enrollments. | `string` | `"CommandCA"` | no | +| [kube\_cluster\_name](#input\_kube\_cluster\_name) | The name of the Kubernetes cluster to use | `string` | `"dev-cluster"` | no | +| [kube\_namespace](#input\_kube\_namespace) | Kubernetes namespace to store the certificate in | `string` | `"default"` | no | +| [kube\_tlssecr\_name](#input\_kube\_tlssecr\_name) | The name of the Kubernetes TLS secret for the Keyfactor Command `k8s-orchestrator` extension to manage | `string` | `"kfc-k8stlssecr-deployment"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | Path to the kubeconfig file | `string` | `"~/.kube/config"` | no | +| [webserver\_template](#input\_webserver\_template) | The webserver template to use in certificate generation | `string` | `"2YearTestWebServer"` | no | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/dev_k8s_cluster/keyfactor_command/enrollments.tf b/dev_k8s_cluster/keyfactor_command/enrollments.tf new file mode 100644 index 0000000..a5e24d2 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/enrollments.tf @@ -0,0 +1,95 @@ +resource "keyfactor_certificate" "pfx_enrollment_00" { + common_name = "K8S PFX Enrollment Certificate 01" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 00"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} + +resource "keyfactor_certificate" "pfx_enrollment_01" { + common_name = "K8S PFX Enrollment Certificate 01" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 01"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} + +resource "keyfactor_certificate" "pfx_enrollment_02" { + common_name = "K8S PFX Enrollment Certificate 02" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 02"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} + +resource "keyfactor_certificate" "pfx_enrollment_03" { + common_name = "K8S PFX Enrollment Certificate 03" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 03"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} + +resource "keyfactor_certificate" "pfx_enrollment_04" { + common_name = "K8S PFX Enrollment Certificate 04" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 04"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} + +resource "keyfactor_certificate" "pfx_enrollment_05" { + common_name = "K8S PFX Enrollment Certificate" + country = "US" + state = "Ohio" + locality = "Cleveland" + organization = "Keyfactor" + organizational_unit = "Engineering" + dns_sans = ["K8S PFX Enrollment Certificate 05"] + // Please don't use this password in production pass in an environmental variable or something + certificate_authority = "${var.default_ca_domain}\\${var.default_cert_ca}" + certificate_template = var.webserver_template + metadata = { + "Email-Contact" = "terraform@example.com" + } +} \ No newline at end of file diff --git a/dev_k8s_cluster/keyfactor_command/kfc_tlssecr_stores.tf b/dev_k8s_cluster/keyfactor_command/kfc_tlssecr_stores.tf new file mode 100644 index 0000000..de89535 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/kfc_tlssecr_stores.tf @@ -0,0 +1,137 @@ +data keyfactor_agent "k8s" { + agent_identifier = var.client_machine_name +} + + + +resource "keyfactor_certificate_store" "tls_store_00" { + client_machine = data.keyfactor_agent.k8s.client_machine + # Orchestrator client name + store_path = "${var.kube_cluster_name}/${var.kube_namespace}/${var.kube_tlssecr_name}00" # Varies based on store type + agent_identifier = data.keyfactor_agent.k8s.agent_identifier + # Orchestrator GUID or Orchestrator ClientMachine + store_type = "K8STLSSecr" # Must exist in KeyFactor + server_username = "kubeconfig" + server_password = file(var.kubeconfig_file) + server_use_ssl = true + inventory_schedule = "5m" + properties = { + KubeSecretType = "tls_secret" +# KubeNamespace = var.kube_namespace # this SHOULD take precedence over the store_path +# KubeSecretName = var.k8stlssecr_name # this SHOULD take precedence over the store_path +# KubeSvcCreds = file(var.kubeconfig_file) # todo: invalid property +# SeparateChain = true # todo: invalid property +# IncludeCertChain = true # todo: invalid property + } +} + +resource "keyfactor_certificate_deployment" "k8stlssecr_00" { + certificate_id = keyfactor_certificate.pfx_enrollment_00.certificate_id + certificate_store_id = keyfactor_certificate_store.tls_store_00.id +} + + + +resource "keyfactor_certificate_store" "tls_store_01" { + client_machine = data.keyfactor_agent.k8s.client_machine + # Orchestrator client name + store_path = "${var.kube_cluster_name}/${var.kube_namespace}/${var.kube_tlssecr_name}01" # Varies based on store type + agent_identifier = data.keyfactor_agent.k8s.agent_identifier + # Orchestrator GUID or Orchestrator ClientMachine + store_type = "K8STLSSecr" # Must exist in KeyFactor + server_username = "kubeconfig" + server_password = file(var.kubeconfig_file) + server_use_ssl = true + inventory_schedule = "5m" + properties = { + KubeSecretType = "tls_secret" + # KubeNamespace = var.kube_namespace # this SHOULD take precedence over the store_path + # KubeSecretName = var.k8stlssecr_name # this SHOULD take precedence over the store_path + # KubeSvcCreds = file(var.kubeconfig_file) # todo: invalid property + # SeparateChain = true # todo: invalid property + # IncludeCertChain = true # todo: invalid property + } +} + +resource "keyfactor_certificate_deployment" "k8stlssecr_01" { + certificate_id = keyfactor_certificate.pfx_enrollment_01.certificate_id + certificate_store_id = keyfactor_certificate_store.tls_store_01.id +} + +resource "keyfactor_certificate_store" "tls_store_02" { + client_machine = data.keyfactor_agent.k8s.client_machine + # Orchestrator client name + store_path = "${var.kube_cluster_name}/${var.kube_namespace}/${var.kube_tlssecr_name}02" # Varies based on store type + agent_identifier = data.keyfactor_agent.k8s.agent_identifier + # Orchestrator GUID or Orchestrator ClientMachine + store_type = "K8STLSSecr" # Must exist in KeyFactor + server_username = "kubeconfig" + server_password = file(var.kubeconfig_file) + server_use_ssl = true + inventory_schedule = "5m" + properties = { + KubeSecretType = "tls_secret" + # KubeNamespace = var.kube_namespace # this SHOULD take precedence over the store_path + # KubeSecretName = var.k8stlssecr_name # this SHOULD take precedence over the store_path + # KubeSvcCreds = file(var.kubeconfig_file) # todo: invalid property + # SeparateChain = true # todo: invalid property + # IncludeCertChain = true # todo: invalid property + } +} + +resource "keyfactor_certificate_deployment" "k8stlssecr_02" { + certificate_id = keyfactor_certificate.pfx_enrollment_02.certificate_id + certificate_store_id = keyfactor_certificate_store.tls_store_02.id +} + +resource "keyfactor_certificate_store" "tls_store_03" { + client_machine = data.keyfactor_agent.k8s.client_machine + # Orchestrator client name + store_path = "${var.kube_cluster_name}/${var.kube_namespace}/${var.kube_tlssecr_name}03" # Varies based on store type + agent_identifier = data.keyfactor_agent.k8s.agent_identifier + # Orchestrator GUID or Orchestrator ClientMachine + store_type = "K8STLSSecr" # Must exist in KeyFactor + server_username = "kubeconfig" + server_password = file(var.kubeconfig_file) + server_use_ssl = true + inventory_schedule = "5m" + properties = { + KubeSecretType = "tls_secret" + # KubeNamespace = var.kube_namespace # this SHOULD take precedence over the store_path + # KubeSecretName = var.k8stlssecr_name # this SHOULD take precedence over the store_path + # KubeSvcCreds = file(var.kubeconfig_file) # todo: invalid property + # SeparateChain = true # todo: invalid property + # IncludeCertChain = true # todo: invalid property + } +} + +resource "keyfactor_certificate_deployment" "k8stlssecr_03" { + certificate_id = keyfactor_certificate.pfx_enrollment_03.certificate_id + certificate_store_id = keyfactor_certificate_store.tls_store_03.id +} + +resource "keyfactor_certificate_store" "tls_store_04" { + client_machine = data.keyfactor_agent.k8s.client_machine + # Orchestrator client name + store_path = "${var.kube_cluster_name}/${var.kube_namespace}/${var.kube_tlssecr_name}04" # Varies based on store type + agent_identifier = data.keyfactor_agent.k8s.agent_identifier + # Orchestrator GUID or Orchestrator ClientMachine + store_type = "K8STLSSecr" # Must exist in KeyFactor + server_username = "kubeconfig" + server_password = file(var.kubeconfig_file) + server_use_ssl = true + inventory_schedule = "5m" + properties = { + KubeSecretType = "tls_secret" + # KubeNamespace = var.kube_namespace # this SHOULD take precedence over the store_path + # KubeSecretName = var.k8stlssecr_name # this SHOULD take precedence over the store_path + # KubeSvcCreds = file(var.kubeconfig_file) # todo: invalid property + # SeparateChain = true # todo: invalid property + # IncludeCertChain = true # todo: invalid property + } +} + +resource "keyfactor_certificate_deployment" "k8stlssecr_04" { + certificate_id = keyfactor_certificate.pfx_enrollment_04.certificate_id + certificate_store_id = keyfactor_certificate_store.tls_store_04.id +} \ No newline at end of file diff --git a/dev_k8s_cluster/keyfactor_command/providers.tf b/dev_k8s_cluster/keyfactor_command/providers.tf new file mode 100644 index 0000000..d7d51c1 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5" + required_providers { + keyfactor = { + source = "keyfactor-pub/keyfactor" + version = ">=2.1.11" + } + } +} + +provider "keyfactor" { + +} + diff --git a/dev_k8s_cluster/keyfactor_command/variables.tf b/dev_k8s_cluster/keyfactor_command/variables.tf new file mode 100644 index 0000000..30efbe3 --- /dev/null +++ b/dev_k8s_cluster/keyfactor_command/variables.tf @@ -0,0 +1,58 @@ +variable "kfc_ca_domain" { + type = string + description = "The default CA domain to use for the certificate" + default = "Keyfactor" +} + +variable "kfc_ca_name" { + type = string + description = "The name of the certificate authority to use for the Keyfactor Command certificate enrollments." + default = "CommandCA" +} + +variable "client_machine_name" { + type = string + description = "Name of the client machine name of the Keyfactor Command Universal Orchestrator to use." +} + +variable "kubeconfig_file" { + type = string + description = "Path to the kubeconfig file" + default = "~/.kube/config" +} + +variable "kube_namespace" { + type = string + description = "Kubernetes namespace to store the certificate in" + default = "default" +} + +variable "webserver_template" { + type = string + description = "The webserver template to use in certificate generation" + default = "2YearTestWebServer" +} + +variable "default_cert_ca" { + type = string + description = "The default certificate authority to use in certificate generation" + default = "CommandCA1" +} + +variable "default_ca_domain" { + type = string + description = "The default certificate authority domain to use in certificate generation" + default = "DC-CA.Command.local" +} + +variable "kube_cluster_name" { + type = string + description = "The name of the Kubernetes cluster to use" + default = "dev-cluster" +} + +variable "kube_tlssecr_name" { + type = string + description = "The name of the Kubernetes TLS secret for the Keyfactor Command `k8s-orchestrator` extension to manage" + default = "kfc-k8stlssecr-deployment" +} \ No newline at end of file diff --git a/dev_k8s_cluster/namespaces.tf b/dev_k8s_cluster/namespaces.tf new file mode 100644 index 0000000..cf93480 --- /dev/null +++ b/dev_k8s_cluster/namespaces.tf @@ -0,0 +1,17 @@ +resource "kubernetes_namespace" "keyfactor_command" { + metadata { + name = "keyfactor-command" + } +} + +resource "kubernetes_namespace" "test" { + metadata { + name = "test" + } +} + +data "kubernetes_namespace" "dashboard" { + metadata { + name = "kubernetes-dashboard" + } +} diff --git a/dev_k8s_cluster/providers.tf b/dev_k8s_cluster/providers.tf new file mode 100644 index 0000000..a995f1b --- /dev/null +++ b/dev_k8s_cluster/providers.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.5" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">=2.30" + } + } +} + +provider "kubernetes" { + config_path = "~/.kube/config" + config_context = "docker-desktop" +} + diff --git a/dev_k8s_cluster/role_bindings.tf b/dev_k8s_cluster/role_bindings.tf new file mode 100644 index 0000000..50bf28c --- /dev/null +++ b/dev_k8s_cluster/role_bindings.tf @@ -0,0 +1,15 @@ +resource "kubernetes_cluster_role_binding" "example" { + metadata { + name = kubernetes_service_account.admin_user.metadata.0.name + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "cluster-admin" + } + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.admin_user.metadata.0.name + namespace = kubernetes_service_account.admin_user.metadata.0.namespace + } +} \ No newline at end of file diff --git a/dev_k8s_cluster/service_accounts.tf b/dev_k8s_cluster/service_accounts.tf new file mode 100644 index 0000000..f53599e --- /dev/null +++ b/dev_k8s_cluster/service_accounts.tf @@ -0,0 +1,24 @@ +resource "kubernetes_service_account" "admin_user" { + metadata { + name = "admin-user" + namespace = data.kubernetes_namespace.dashboard.metadata.0.name + } +} + +resource "kubernetes_secret" "admin_user_token" { + metadata { + name = kubernetes_service_account.admin_user.metadata.0.name + namespace = kubernetes_service_account.admin_user.metadata.0.namespace + annotations = { + "kubernetes.io/service-account.name" = kubernetes_service_account.admin_user.metadata.0.name + } + } + + type = "kubernetes.io/service-account-token" + wait_for_service_account_token = true +} + +output "admin_user_token" { + value = kubernetes_secret.admin_user_token.data.token + sensitive = true +} \ No newline at end of file diff --git a/dev_k8s_cluster/setup_dashboard.sh b/dev_k8s_cluster/setup_dashboard.sh new file mode 100644 index 0000000..e33b59e --- /dev/null +++ b/dev_k8s_cluster/setup_dashboard.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +kubectl config use-context docker-desktop +helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/ +helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard +kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443 \ No newline at end of file diff --git a/integration-manifest.json b/integration-manifest.json index 7c0bd59..cdb1d09 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -3,7 +3,7 @@ "integration_type": "orchestrator", "name": "Kubernetes Orchestrator Extension", "status": "production", - "description": "The Kubernetes Orchestrator allows for the remote management of certificate stores defined in a Kubernetes cluster. The following types of Kubernetes resources are supported:\n- Secrets - Kubernetes secrets of type `kubernetes.io/tls` or `Opaque` \n- Certificates - Kubernetes certificates of type `certificates.k8s.io/v1`", + "description": "The Kubernetes Orchestrator allows for the remote management of certificate stores defined in a Kubernetes cluster. The following types of Kubernetes resources are supported: kubernetes secrets of `kubernetes.io/tls` or `Opaque` and kubernetes certificates `certificates.k8s.io/v1`", "short_description": "The Kubernetes Orchestrator allows for remote management of Kubernetes secret types 'Opaque' and 'kubernetes.io/tls' as well as 'certificates.k8s.io/v1' resources. ", "link_github": true, "topics": [ @@ -20,9 +20,11 @@ ], "update_catalog": true, "support_level": "kf-supported", + "release_dir": "kubernetes-orchestrator-extension\\bin\\Release", "about": { "orchestrator": { - "UOFramework": "10.1", + "keyfactor_platform_version": "10.x", + "UOFramework": "10.x", "pam_support": true, "win": { "supportsCreateStore": true, @@ -79,30 +81,6 @@ "DependsOn": "", "DefaultValue": "cert", "Required": true - }, - { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": false - }, - { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", - "Type": "Bool", - "DependsOn": "", - "DefaultValue": "true", - "Required": true } ], "EntryParameters": null, @@ -134,28 +112,18 @@ }, "Properties": [ { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, + "Name": "SeparateChain", + "DisplayName": "Separate Certificate Chain", + "Type": "Bool", + "DefaultValue": "false", "Required": false }, { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", + "Name": "IncludeCertChain", + "DisplayName": "Include Certificate Chain", "Type": "Bool", - "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": false } ], "EntryParameters": null, @@ -215,8 +183,8 @@ "DisplayName": "CertificateDataFieldName", "Type": "String", "DependsOn": "", - "DefaultValue": null, - "Required": false + "DefaultValue": ".jks", + "Required": true }, { "Name": "PasswordFieldName", @@ -304,28 +272,18 @@ "Required": false }, { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, + "Name": "SeparateChain", + "DisplayName": "Separate Certificate Chain", + "Type": "Bool", + "DefaultValue": "false", "Required": false }, { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", + "Name": "IncludeCertChain", + "DisplayName": "Include Certificate Chain", "Type": "Bool", - "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": false } ], "EntryParameters": null, @@ -356,6 +314,14 @@ "Remove": true }, "Properties": [ + { + "Name": "KubeSecretType", + "DisplayName": "Kube Secret Type", + "Type": "String", + "DependsOn": "", + "DefaultValue": "pkcs12", + "Required": true + }, { "Name": "KubeSecretKey", "DisplayName": "Kube Secret Key", @@ -364,6 +330,14 @@ "DefaultValue": "pfx", "Required": false }, + { + "Name": "CertificateDataFieldName", + "DisplayName": "CertificateDataFieldName", + "Type": "String", + "DependsOn": "", + "DefaultValue": ".p12", + "Required": true + }, { "Name": "PasswordFieldName", "DisplayName": "Password Field Name", @@ -396,38 +370,6 @@ "DefaultValue": null, "Required": false }, - { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": false - }, - { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", - "Type": "Bool", - "DependsOn": "", - "DefaultValue": "true", - "Required": true - }, - { - "Name": "KubeSecretType", - "DisplayName": "Kube Secret Type", - "Type": "String", - "DependsOn": "", - "DefaultValue": "pkcs12", - "Required": true - }, { "Name": "StorePasswordPath", "DisplayName": "StorePasswordPath", @@ -490,28 +432,18 @@ "Required": true }, { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, + "Name": "SeparateChain", + "DisplayName": "Separate Certificate Chain", + "Type": "Bool", + "DefaultValue": "false", "Required": false }, { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", + "Name": "IncludeCertChain", + "DisplayName": "Include Certificate Chain", "Type": "Bool", - "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": false } ], "EntryParameters": null, @@ -567,28 +499,18 @@ "Required": true }, { - "Name": "ServerUsername", - "DisplayName": "Server Username", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, + "Name": "SeparateChain", + "DisplayName": "Separate Certificate Chain", + "Type": "Bool", + "DefaultValue": "false", "Required": false }, { - "Name": "ServerPassword", - "DisplayName": "Server Password", - "Type": "Secret", - "DependsOn": "", - "DefaultValue": null, - "Required": true - }, - { - "Name": "ServerUseSsl", - "DisplayName": "Use SSL", + "Name": "IncludeCertChain", + "DisplayName": "Include Certificate Chain", "Type": "Bool", - "DependsOn": "", "DefaultValue": "true", - "Required": true + "Required": false } ], "EntryParameters": null, diff --git a/kubernetes-orchestrator-extension/Clients/KubeClient.cs b/kubernetes-orchestrator-extension/Clients/KubeClient.cs index 791e394..1124129 100644 --- a/kubernetes-orchestrator-extension/Clients/KubeClient.cs +++ b/kubernetes-orchestrator-extension/Clients/KubeClient.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, @@ -16,6 +16,7 @@ using System.Text; using k8s; using k8s.Autorest; +using k8s.Exceptions; using k8s.KubeConfigModels; using k8s.Models; using Keyfactor.Extensions.Orchestrator.K8S.Jobs; @@ -35,21 +36,16 @@ namespace Keyfactor.Extensions.Orchestrator.K8S.Clients; public class KubeCertificateManagerClient { + private readonly ILogger _logger; - private ILogger _logger; - - private string ConfigJson { get; set; } - - private K8SConfiguration ConfigObj { get; set; } - - public KubeCertificateManagerClient(string kubeconfig) + public KubeCertificateManagerClient(string kubeconfig, bool useSSL = true) { _logger = LogHandler.GetClassLogger(MethodBase.GetCurrentMethod()?.DeclaringType); Client = GetKubeClient(kubeconfig); ConfigJson = kubeconfig; try { - ConfigObj = ParseKubeConfig(kubeconfig); + ConfigObj = ParseKubeConfig(kubeconfig, !useSSL); // invert useSSL to skip TLS verification } catch (Exception) { @@ -57,6 +53,10 @@ public KubeCertificateManagerClient(string kubeconfig) } } + private string ConfigJson { get; set; } + + private K8SConfiguration ConfigObj { get; } + private IKubernetes Client { get; set; } public string GetClusterName() @@ -64,13 +64,14 @@ public string GetClusterName() _logger.LogTrace("Entered GetClusterName()"); try { + _logger.LogTrace("Returning cluster name from ConfigObj"); return ConfigObj.Clusters.FirstOrDefault()?.Name; } catch (Exception) { + _logger.LogWarning("Error getting cluster name from ConfigObj attempting to return client base uri"); return GetHost(); } - } public string GetHost() @@ -79,33 +80,48 @@ public string GetHost() return Client.BaseUri.ToString(); } - private K8SConfiguration ParseKubeConfig(string kubeconfig) + private K8SConfiguration ParseKubeConfig(string kubeconfig, bool skipTLSVerify = false) { _logger.LogTrace("Entered ParseKubeConfig()"); var k8SConfiguration = new K8SConfiguration(); - // test if kubeconfig is base64 encoded - _logger.LogDebug("Testing if kubeconfig is base64 encoded"); + + _logger.LogTrace("Checking if kubeconfig is null or empty"); + if (string.IsNullOrEmpty(kubeconfig)) + { + _logger.LogError("kubeconfig is null or empty"); + throw new KubeConfigException("kubeconfig is null or empty, please provide a valid kubeconfig in JSON format. For more information on how to create a kubeconfig file, please visit https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#example-service-account-json"); + } + try { + // test if kubeconfig is base64 encoded + _logger.LogDebug("Testing if kubeconfig is base64 encoded"); var decodedKubeconfig = Encoding.UTF8.GetString(Convert.FromBase64String(kubeconfig)); kubeconfig = decodedKubeconfig; _logger.LogDebug("Successfully decoded kubeconfig from base64"); } catch { - // not base64 encoded so do nothing + _logger.LogTrace("Kubeconfig is not base64 encoded"); } - // check if json is escaped + _logger.LogTrace("Checking if kubeconfig is escaped JSON"); if (kubeconfig.StartsWith("\\")) { - _logger.LogDebug("Unescaping kubeconfig JSON"); + _logger.LogDebug("Un-escaping kubeconfig JSON"); kubeconfig = kubeconfig.Replace("\\", ""); kubeconfig = kubeconfig.Replace("\\n", "\n"); + _logger.LogDebug("Successfully un-escaped kubeconfig JSON"); } // parse kubeconfig as a dictionary of string, string - if (!kubeconfig.StartsWith("{")) return k8SConfiguration; + if (!kubeconfig.StartsWith("{")) + { + _logger.LogError("kubeconfig is not a JSON object"); + throw new KubeConfigException("kubeconfig is not a JSON object, please provide a valid kubeconfig in JSON format. For more information on how to create a kubeconfig file, please visit: https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#get_service_account_credssh"); + // return k8SConfiguration; + } + _logger.LogDebug("Parsing kubeconfig as a dictionary of string, string"); @@ -132,6 +148,21 @@ private K8SConfiguration ParseKubeConfig(string kubeconfig) _logger.LogTrace("Entering foreach loop to parse clusters..."); foreach (var clusterMetadata in JsonConvert.DeserializeObject(cl.ToString() ?? string.Empty)) { + _logger.LogTrace("Creating Cluster object for cluster '{Name}'", clusterMetadata["name"]?.ToString()); + // get environment variable for skip tls verify and convert to bool + var skipTlsEnvStr = Environment.GetEnvironmentVariable("KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY"); + _logger.LogTrace("KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY environment variable: {SkipTlsVerify}", skipTlsEnvStr); + if (!string.IsNullOrEmpty(skipTlsEnvStr) && (bool.TryParse(skipTlsEnvStr, out var skipTlsVerifyEnv) || skipTlsEnvStr == "1")) + { + if (skipTlsEnvStr == "1") skipTlsVerifyEnv = true; + _logger.LogDebug("Setting skip-tls-verify to {SkipTlsVerify}", skipTlsVerifyEnv); + if (skipTlsVerifyEnv && !skipTLSVerify) + { + _logger.LogWarning("Skipping TLS verification is enabled in environment variable KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY this takes the highest precedence and verification will be skipped. To disable this, set the environment variable to 'false' or remove it"); + skipTLSVerify = true; + } + } + var clusterObj = new Cluster { Name = clusterMetadata["name"]?.ToString(), @@ -139,12 +170,13 @@ private K8SConfiguration ParseKubeConfig(string kubeconfig) { Server = clusterMetadata["cluster"]?["server"]?.ToString(), CertificateAuthorityData = clusterMetadata["cluster"]?["certificate-authority-data"]?.ToString(), - SkipTlsVerify = false + SkipTlsVerify = skipTLSVerify } }; _logger.LogTrace("Adding cluster '{Name}'({@Endpoint}) to K8SConfiguration", clusterObj.Name, clusterObj.ClusterEndpoint); k8SConfiguration.Clusters = new List { clusterObj }; } + _logger.LogTrace("Finished parsing clusters"); _logger.LogDebug("Parsing users"); @@ -164,6 +196,7 @@ private K8SConfiguration ParseKubeConfig(string kubeconfig) _logger.LogTrace("Adding user {Name} to K8SConfiguration object", userObj.Name); k8SConfiguration.Users = new List { userObj }; } + _logger.LogTrace("Finished parsing users"); _logger.LogDebug("Parsing contexts"); @@ -184,8 +217,10 @@ private K8SConfiguration ParseKubeConfig(string kubeconfig) _logger.LogTrace("Adding context '{Name}' to K8SConfiguration object", contextObj.Name); k8SConfiguration.Contexts = new List { contextObj }; } + _logger.LogTrace("Finished parsing contexts"); _logger.LogDebug("Finished parsing kubeconfig"); + return k8SConfiguration; } @@ -205,30 +240,40 @@ private IKubernetes GetKubeClient(string kubeconfig) _logger.LogDebug("Calling ParseKubeConfig()"); var k8SConfiguration = ParseKubeConfig(kubeconfig); _logger.LogDebug("Finished calling ParseKubeConfig()"); - + // use k8sConfiguration over credentialFileName KubernetesClientConfiguration config; if (k8SConfiguration != null) // Config defined in store parameters takes highest precedence { - _logger.LogDebug("Config defined in store parameters takes highest precedence - calling BuildConfigFromConfigObject()"); - config = KubernetesClientConfiguration.BuildConfigFromConfigObject(k8SConfiguration); - _logger.LogDebug("Finished calling BuildConfigFromConfigObject()"); + try + { + _logger.LogDebug( + "Config defined in store parameters takes highest precedence - calling BuildConfigFromConfigObject()"); + config = KubernetesClientConfiguration.BuildConfigFromConfigObject(k8SConfiguration); + _logger.LogDebug("Finished calling BuildConfigFromConfigObject()"); + } + catch (Exception e) + { + _logger.LogError("Error building config from config object: {Error}", e.Message); + config = KubernetesClientConfiguration.BuildDefaultConfig(); + } } - else if (credentialFileName == "") // If no config defined in store parameters, use default config. This should never happen though. + else if (string.IsNullOrEmpty(credentialFileName)) // If no config defined in store parameters, use default config. This should never happen though. { - _logger.LogWarning("No config defined in store parameters, using default config. This should never happen though"); + _logger.LogWarning( + "No config defined in store parameters, using default config. This should never happen!"); config = KubernetesClientConfiguration.BuildDefaultConfig(); _logger.LogDebug("Finished calling BuildDefaultConfig()"); } else { - // Logger.LogDebug($"Attempting to load config from file {credentialFileName}"); - config = KubernetesClientConfiguration.BuildConfigFromConfigFile(strWorkPath != null && !credentialFileName.Contains(strWorkPath) - ? Path.Join(strWorkPath, credentialFileName) - : // Else attempt to load config from file - credentialFileName); // Else attempt to load config from file + _logger.LogDebug("Calling BuildConfigFromConfigFile()"); + config = KubernetesClientConfiguration.BuildConfigFromConfigFile( + strWorkPath != null && !credentialFileName.Contains(strWorkPath) + ? Path.Join(strWorkPath, credentialFileName) + : // Else attempt to load config from file + credentialFileName); // Else attempt to load config from file _logger.LogDebug("Finished calling BuildConfigFromConfigFile()"); - } _logger.LogDebug("Creating Kubernetes client"); @@ -252,7 +297,7 @@ public X509Certificate2 FindCertificateByCN(X509Certificate2Collection certifica public X509Certificate2 FindCertificateByThumbprint(X509Certificate2Collection certificates, string thumbprint) { - X509Certificate2 foundCertificate = certificates + var foundCertificate = certificates .OfType() .FirstOrDefault(cert => cert.Thumbprint == thumbprint); @@ -261,16 +306,18 @@ public X509Certificate2 FindCertificateByThumbprint(X509Certificate2Collection c public X509Certificate2 FindCertificateByAlias(X509Certificate2Collection certificates, string alias) { - X509Certificate2 foundCertificate = certificates + var foundCertificate = certificates .OfType() - .FirstOrDefault(cert => cert.SubjectName.Name.Contains(alias)); + .FirstOrDefault(cert => cert.SubjectName.Name != null && cert.SubjectName.Name.Contains(alias)); return foundCertificate; } - public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName, string namespaceName, string secretType, string certdataFieldName, + public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName, + string namespaceName, string secretType, string certDataFieldName, string storePasswd, V1Secret k8SSecretData, - bool append = false, bool overwrite = true, bool passwdIsK8sSecret = false, string passwordSecretPath = "", string passwordFieldName = "password", + bool append = false, bool overwrite = true, bool passwdIsK8SSecret = false, string passwordSecretPath = "", + string passwordFieldName = "password", string[] certdataFieldNames = null) { _logger.LogTrace("Entered UpdatePKCS12SecretStore()"); @@ -282,7 +329,7 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st var existingPkcs12 = new X509Certificate2Collection(); var newPkcs12Collection = new X509Certificate2Collection(); var k8sCollection = new X509Certificate2Collection(); - byte[] storePasswordBytes = Encoding.UTF8.GetBytes(""); + var storePasswordBytes = Encoding.UTF8.GetBytes(""); if (existingPkcs12DataObj?.Data == null) { @@ -292,19 +339,18 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st { _logger.LogTrace("existingPkcs12DataObj.Data is not null"); - // KeyValuePair updated_data = new KeyValuePair(); - foreach (var fieldName in existingPkcs12DataObj?.Data.Keys) { //check if key is in certdataFieldNames //if fieldname contains a . then split it and use the last part var searchFieldName = fieldName; - certdataFieldName = fieldName; + certDataFieldName = fieldName; if (fieldName.Contains(".")) { var splitFieldName = fieldName.Split("."); searchFieldName = splitFieldName[splitFieldName.Length - 1]; } + if (certdataFieldNames != null && !certdataFieldNames.Contains(searchFieldName)) continue; _logger.LogTrace($"Adding cert '{fieldName}' to existingPkcs12"); @@ -319,25 +365,30 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st var k8sPasswordObj = ReadBuddyPass(passwordSecretName, passwordNamespace); storePasswordBytes = k8sPasswordObj.Data[passwordFieldName]; var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], storePasswdString, X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], storePasswdString, + X509KeyStorageFlags.Exportable); } else { storePasswordBytes = existingPkcs12DataObj.Data[passwordFieldName]; - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } } else if (!string.IsNullOrEmpty(jobCertificate.StorePassword)) { storePasswordBytes = Encoding.UTF8.GetBytes(jobCertificate.StorePassword); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } else { storePasswordBytes = Encoding.UTF8.GetBytes(storePasswd); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } } + if (existingPkcs12.Count > 0) { // Check if overwrite is true, if so, replace existing cert with new cert @@ -345,7 +396,7 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st { _logger.LogTrace("Overwrite is true, replacing existing cert with new cert"); - X509Certificate2 foundCertificate = FindCertificateByAlias(existingPkcs12, jobCertificate.Alias); + var foundCertificate = FindCertificateByAlias(existingPkcs12, jobCertificate.Alias); if (foundCertificate != null) { // Certificate found @@ -378,28 +429,26 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st Type = "Opaque", Data = new Dictionary { - { certdataFieldName, p12bytes } + { certDataFieldName, p12bytes } } }; switch (string.IsNullOrEmpty(storePasswd)) { - case false when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret: // password is not empty and passwordSecretPath is empty + case false + when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8SSecret + : // password is not empty and passwordSecretPath is empty { _logger.LogDebug("Adding password to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "password"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password"; secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(storePasswd)); break; } - case false when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret: // password is not empty and passwordSecretPath is not empty + case false + when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8SSecret + : // password is not empty and passwordSecretPath is not empty { _logger.LogDebug("Adding password secret path to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "passwordSecretPath"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "passwordSecretPath"; secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath)); // Lookup password secret path on cluster to see if it exists @@ -408,24 +457,32 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st // Assume secret pattern is namespace/secretName var passwordSecretName = splitPasswordPath[splitPasswordPath.Length - 1]; var passwordSecretNamespace = splitPasswordPath[0]; - _logger.LogDebug($"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); try { - var passwordSecret = Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); + var passwordSecret = + Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); // storePasswd = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]); - _logger.LogDebug($"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}"); // Update secret - _logger.LogDebug($"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}"); passwordSecret.Data[passwordFieldName] = Encoding.UTF8.GetBytes(storePasswd); - var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret, passwordSecretName, passwordSecretNamespace); - _logger.LogDebug($"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret, + passwordSecretName, passwordSecretNamespace); + _logger.LogDebug( + $"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}"); } catch (HttpOperationException e) { - _logger.LogError($"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogError( + $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); _logger.LogError(e.Message); // Attempt to create a new secret - _logger.LogDebug($"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); var passwordSecretData = new V1Secret { Metadata = new V1ObjectMeta @@ -438,9 +495,11 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st { passwordFieldName, Encoding.UTF8.GetBytes(storePasswd) } } }; - var createdPasswordSecret = Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); + var createdPasswordSecret = + Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); _logger.LogDebug("Successfully created secret " + passwordSecretPath); } + break; } } @@ -455,9 +514,11 @@ public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, st return updatedSecret; } - public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName, string namespaceName, string secretType, string certdataFieldName, + public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName, string namespaceName, + string secretType, string certdataFieldName, string storePasswd, V1Secret k8SSecretData, - bool append = false, bool overwrite = true, bool passwdIsK8sSecret = false, string passwordSecretPath = "", string passwordFieldName = "password", + bool append = false, bool overwrite = true, bool passwdIsK8sSecret = false, string passwordSecretPath = "", + string passwordFieldName = "password", string[] certdataFieldNames = null, bool remove = false) { _logger.LogTrace("Entered UpdatePKCS12SecretStore()"); @@ -471,7 +532,7 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string var existingPkcs12 = new X509Certificate2Collection(); var newPkcs12Collection = new X509Certificate2Collection(); var k8sCollection = new X509Certificate2Collection(); - byte[] storePasswordBytes = Encoding.UTF8.GetBytes(""); + var storePasswordBytes = Encoding.UTF8.GetBytes(""); if (existingPkcs12DataObj?.Data == null) { @@ -493,6 +554,7 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string var splitFieldName = fieldName.Split("."); searchFieldName = splitFieldName[splitFieldName.Length - 1]; } + if (certdataFieldNames != null && !certdataFieldNames.Contains(searchFieldName)) continue; certdataFieldName = fieldName; @@ -508,31 +570,36 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string var k8sPasswordObj = ReadBuddyPass(passwordSecretName, passwordNamespace); storePasswordBytes = k8sPasswordObj.Data[passwordFieldName]; var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], storePasswdString, X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], storePasswdString, + X509KeyStorageFlags.Exportable); } else { storePasswordBytes = existingPkcs12DataObj.Data[passwordFieldName]; - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } } else if (!string.IsNullOrEmpty(jobCertificate.StorePassword)) { storePasswordBytes = Encoding.UTF8.GetBytes(jobCertificate.StorePassword); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } else { storePasswordBytes = Encoding.UTF8.GetBytes(storePasswd); - existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); + existingPkcs12.Import(existingPkcs12DataObj.Data[fieldName], + Encoding.UTF8.GetString(storePasswordBytes), X509KeyStorageFlags.Exportable); } } + if (existingPkcs12.Count > 0) { // create x509Certificate2 from jobCertificate.CertBytes if (remove) { - X509Certificate2 foundCertificate = FindCertificateByAlias(existingPkcs12, jobCertificate.Alias); + var foundCertificate = FindCertificateByAlias(existingPkcs12, jobCertificate.Alias); if (foundCertificate != null) { // Certificate found @@ -543,7 +610,8 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string } else { - var newCert = new X509Certificate2(jobCertificate.CertBytes, storePasswd, X509KeyStorageFlags.Exportable); + var newCert = new X509Certificate2(jobCertificate.CertBytes, storePasswd, + X509KeyStorageFlags.Exportable); var newCertCn = newCert.GetNameInfo(X509NameType.SimpleName, false); //import jobCertificate.CertBytes into existingPkcs12 @@ -552,12 +620,13 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string { _logger.LogTrace("Overwrite is true, replacing existing cert with new cert"); - X509Certificate2 foundCertificate = FindCertificateByCN(existingPkcs12, newCertCn); + var foundCertificate = FindCertificateByCN(existingPkcs12, newCertCn); if (foundCertificate != null) { // Certificate found // replace the found certificate with the new certificate - _logger.LogTrace("Certificate found, replacing the found certificate with the new certificate"); + _logger.LogTrace( + "Certificate found, replacing the found certificate with the new certificate"); existingPkcs12.Remove(foundCertificate); existingPkcs12.Add(newCert); } @@ -571,6 +640,7 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string } } } + _logger.LogTrace("Importing jobCertificate.CertBytes into existingPkcs12"); k8sCollection = existingPkcs12; } @@ -579,7 +649,6 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string newPkcs12Collection.Import(jobCertificate.CertBytes, storePasswd, X509KeyStorageFlags.Exportable); k8sCollection = newPkcs12Collection; } - } _logger.LogTrace("Creating V1Secret object"); @@ -616,23 +685,21 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string switch (string.IsNullOrEmpty(storePasswd)) { - case false when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret: // password is not empty and passwordSecretPath is empty + case false + when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret + : // password is not empty and passwordSecretPath is empty { _logger.LogDebug("Adding password to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "password"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password"; secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(storePasswd)); break; } - case false when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret: // password is not empty and passwordSecretPath is not empty + case false + when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret + : // password is not empty and passwordSecretPath is not empty { _logger.LogDebug("Adding password secret path to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "passwordSecretPath"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "passwordSecretPath"; secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath)); // Lookup password secret path on cluster to see if it exists @@ -641,24 +708,32 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string // Assume secret pattern is namespace/secretName var passwordSecretName = splitPasswordPath[splitPasswordPath.Length - 1]; var passwordSecretNamespace = splitPasswordPath[0]; - _logger.LogDebug($"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); try { - var passwordSecret = Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); + var passwordSecret = + Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); // storePasswd = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]); - _logger.LogDebug($"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}"); // Update secret - _logger.LogDebug($"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}"); passwordSecret.Data[passwordFieldName] = Encoding.UTF8.GetBytes(storePasswd); - var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret, passwordSecretName, passwordSecretNamespace); - _logger.LogDebug($"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret, + passwordSecretName, passwordSecretNamespace); + _logger.LogDebug( + $"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}"); } catch (HttpOperationException e) { - _logger.LogError($"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogError( + $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); _logger.LogError(e.Message); // Attempt to create a new secret - _logger.LogDebug($"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); var passwordSecretData = new V1Secret { Metadata = new V1ObjectMeta @@ -671,9 +746,11 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string { passwordFieldName, Encoding.UTF8.GetBytes(storePasswd) } } }; - var createdPasswordSecret = Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); + var createdPasswordSecret = + Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); _logger.LogDebug("Successfully created secret " + passwordSecretPath); } + break; } } @@ -689,8 +766,10 @@ public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string } public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertificate, string secretName, - string namespaceName, string secretType, bool overwrite = false, string certDataFieldName = "pkcs12", string passwordFieldName = "password", - string passwordSecretPath = "", bool passwordIsK8SSecret = false, string password = "", string[] allowedKeys = null, bool remove = false) + string namespaceName, string secretType, bool overwrite = false, string certDataFieldName = "pkcs12", + string passwordFieldName = "password", + string passwordSecretPath = "", bool passwordIsK8SSecret = false, string password = "", + string[] allowedKeys = null, bool remove = false) { var storePasswd = string.IsNullOrEmpty(password) ? jobCertificate.Password : password; _logger.LogTrace("Entered CreateOrUpdateCertificateStoreSecret()"); @@ -702,11 +781,8 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertif case "pfx": case "jks": if (remove) - { k8SSecretData = new V1Secret(); - } else - { k8SSecretData = CreateOrUpdatePKCS12Secret(secretName, namespaceName, jobCertificate, @@ -715,7 +791,6 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertif passwordFieldName, passwordSecretPath, allowedKeys); - } break; default: k8SSecretData = new V1Secret(); @@ -739,7 +814,8 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertif _logger.LogWarning("Error while attempting to create secret: " + e.Message); if (e.Message.Contains("Conflict") || e.Message.Contains("Unprocessable")) { - _logger.LogDebug($"Secret {secretName} already exists in namespace {namespaceName}, attempting to update secret..."); + _logger.LogDebug( + $"Secret {secretName} already exists in namespace {namespaceName}, attempting to update secret..."); _logger.LogTrace("Calling UpdateSecretStore()"); switch (secretType) { @@ -761,16 +837,18 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertif null, remove); default: - return UpdateSecretStore(secretName, namespaceName, secretType, "", "", k8SSecretData, false, overwrite); + return UpdateSecretStore(secretName, namespaceName, secretType, "", "", k8SSecretData, false, + overwrite); } - } } + _logger.LogError("Unable to create secret for unknown reason."); return k8SSecretData; } - public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certPem, List chainPem, string secretName, + public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certPem, List chainPem, + string secretName, string namespaceName, string secretType, bool append = false, bool overwrite = false, bool remove = false) { _logger.LogTrace("Entered CreateOrUpdateCertificateStoreSecret()"); @@ -798,11 +876,14 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certP _logger.LogWarning("Error while attempting to create secret: " + e.Message); if (e.Message.Contains("Conflict")) { - _logger.LogDebug($"Secret {secretName} already exists in namespace {namespaceName}, attempting to update secret..."); + _logger.LogDebug( + $"Secret {secretName} already exists in namespace {namespaceName}, attempting to update secret..."); _logger.LogTrace("Calling UpdateSecretStore()"); - return UpdateSecretStore(secretName, namespaceName, secretType, certPem, keyPem, k8SSecretData, append, overwrite); + return UpdateSecretStore(secretName, namespaceName, secretType, certPem, keyPem, k8SSecretData, append, + overwrite); } } + _logger.LogError("Unable to create secret for unknown reason."); return null; } @@ -812,82 +893,80 @@ public Pkcs12Store CreatePKCS12Collection(byte[] pkcs12bytes, string currentPass { try { + var storeBuilder = new Pkcs12StoreBuilder(); + var certs = storeBuilder.Build(); - Pkcs12StoreBuilder storeBuilder = new Pkcs12StoreBuilder(); - Pkcs12Store certs = storeBuilder.Build(); - - byte[] newCertBytes = pkcs12bytes; + var newCertBytes = pkcs12bytes; - Pkcs12Store newEntry = storeBuilder.Build(); + var newEntry = storeBuilder.Build(); - X509Certificate2 cert = new X509Certificate2(newCertBytes, currentPassword, X509KeyStorageFlags.Exportable); - byte[] binaryCert = cert.Export(X509ContentType.Pkcs12, currentPassword); + var cert = new X509Certificate2(newCertBytes, currentPassword, X509KeyStorageFlags.Exportable); + var binaryCert = cert.Export(X509ContentType.Pkcs12, currentPassword); - using (MemoryStream ms = new MemoryStream(string.IsNullOrEmpty(currentPassword) ? binaryCert : newCertBytes)) + using (var ms = new MemoryStream(string.IsNullOrEmpty(currentPassword) ? binaryCert : newCertBytes)) { newEntry.Load(ms, string.IsNullOrEmpty(currentPassword) ? new char[0] : currentPassword.ToCharArray()); } - string checkAliasExists = string.Empty; - string alias = cert.Thumbprint; - foreach (string newEntryAlias in newEntry.Aliases) + var checkAliasExists = string.Empty; + var alias = cert.Thumbprint; + foreach (var newEntryAlias in newEntry.Aliases) { if (!newEntry.IsKeyEntry(newEntryAlias)) continue; checkAliasExists = newEntryAlias; - if (certs.ContainsAlias(alias)) - { - certs.DeleteEntry(alias); - } + if (certs.ContainsAlias(alias)) certs.DeleteEntry(alias); certs.SetKeyEntry(alias, newEntry.GetKey(newEntryAlias), newEntry.GetCertificateChain(newEntryAlias)); } if (string.IsNullOrEmpty(checkAliasExists)) { - Org.BouncyCastle.X509.X509Certificate bcCert = DotNetUtilities.FromX509Certificate(cert); - X509CertificateEntry bcEntry = new X509CertificateEntry(bcCert); - if (certs.ContainsAlias(alias)) - { - certs.DeleteEntry(alias); - } + var bcCert = DotNetUtilities.FromX509Certificate(cert); + var bcEntry = new X509CertificateEntry(bcCert); + if (certs.ContainsAlias(alias)) certs.DeleteEntry(alias); certs.SetCertificateEntry(alias, bcEntry); } - using (MemoryStream outStream = new MemoryStream()) + using (var outStream = new MemoryStream()) { - certs.Save(outStream, string.IsNullOrEmpty(newPassword) ? new char[0] : newPassword.ToCharArray(), new Org.BouncyCastle.Security.SecureRandom()); + certs.Save(outStream, string.IsNullOrEmpty(newPassword) ? new char[0] : newPassword.ToCharArray(), + new SecureRandom()); } + return certs; } catch (Exception ex) { - throw new Exception($"Error attempting to add certficate for store path=StorePath, file name=StoreFileName.", ex); + throw new Exception("Error attempting to add certficate for store path=StorePath, file name=StoreFileName.", + ex); } - } - public X509Certificate2Collection CreatePKCS12Collection(X509Certificate2Collection certificateCollection, string currentPassword, string newPassword) + public X509Certificate2Collection CreatePKCS12Collection(X509Certificate2Collection certificateCollection, + string currentPassword, string newPassword) { // Iterate over the certificates in the collection - foreach (X509Certificate2 certificate in certificateCollection) + foreach (var certificate in certificateCollection) { // Export the private key to a byte array - byte[] privateKeyBytes = certificate.Export(X509ContentType.Pkcs12, currentPassword); + var privateKeyBytes = certificate.Export(X509ContentType.Pkcs12, currentPassword); // Import the private key with the new password - X509Certificate2 newCertificate = new X509Certificate2(privateKeyBytes, newPassword, X509KeyStorageFlags.Exportable); + var newCertificate = new X509Certificate2(privateKeyBytes, newPassword, X509KeyStorageFlags.Exportable); // Replace the certificate in the collection with the new certificate - int index = certificateCollection.IndexOf(certificate); + var index = certificateCollection.IndexOf(certificate); certificateCollection.RemoveAt(index); certificateCollection.Insert(index, newCertificate); } + return certificateCollection; } - private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceName, K8SJobCertificate certObj, string secretFieldName, string password, + private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceName, K8SJobCertificate certObj, + string secretFieldName, string password, string passwordFieldName, string passwordSecretPath = "", string[] allowedKeys = null) { _logger.LogTrace("Entered CreateOrUpdatePKCS12Secret()"); @@ -901,10 +980,7 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN catch (HttpOperationException e) { _logger.LogDebug("Error while attempting to read existing secret: " + e.Message); - if (e.Message.Contains("Not Found")) - { - _logger.LogDebug("No existing secret found."); - } + if (e.Message.Contains("Not Found")) _logger.LogDebug("No existing secret found."); existingSecret = null; } @@ -925,7 +1001,7 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN false, passwordSecretPath, passwordFieldName, - allowedKeys); + allowedKeys); } _logger.LogDebug("Attempting to create new secret..."); @@ -940,7 +1016,7 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN var pkcs12Data = CreatePKCS12Collection(certObj.Pkcs12, password, passwordToWrite); byte[] p12Bytes; - using (MemoryStream stream = new MemoryStream()) + using (var stream = new MemoryStream()) { pkcs12Data.Save(stream, passwordToWrite.ToCharArray(), new SecureRandom()); @@ -950,10 +1026,7 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN // Use the pkcs12Bytes as desired } - if (string.IsNullOrEmpty(secretFieldName)) - { - secretFieldName = "pkcs12"; - } + if (string.IsNullOrEmpty(secretFieldName)) secretFieldName = "pkcs12"; var k8SSecretData = new V1Secret { Metadata = new V1ObjectMeta @@ -970,13 +1043,11 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN switch (string.IsNullOrEmpty(password)) { case false - when certObj.PasswordIsK8SSecret && string.IsNullOrEmpty(certObj.StorePasswordPath): // This means the password is expected to be on the secret so add it + when certObj.PasswordIsK8SSecret && string.IsNullOrEmpty(certObj.StorePasswordPath) + : // This means the password is expected to be on the secret so add it { _logger.LogDebug("Adding password to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "password"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password"; // var passwordToWrite = !string.IsNullOrEmpty(certObj.StorePassword) ? certObj.StorePassword : password; @@ -986,10 +1057,7 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN case false when !string.IsNullOrEmpty(passwordSecretPath): { _logger.LogDebug("Adding password secret path to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "password"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password"; // k8SSecretData.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath)); // Lookup password secret path on cluster to see if it exists @@ -998,18 +1066,22 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN // Assume secret pattern is namespace/secretName var passwordSecretName = splitPasswordPath[splitPasswordPath.Length - 1]; var passwordSecretNamespace = splitPasswordPath[0]; - _logger.LogDebug($"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}"); try { - var passwordSecret = Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); + var passwordSecret = + Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace); password = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]); } catch (HttpOperationException e) { - _logger.LogError($"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogError( + $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); _logger.LogError(e.Message); // Attempt to create a new secret - _logger.LogDebug($"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); // var passwordToWrite = !string.IsNullOrEmpty(certObj.StorePassword) ? certObj.StorePassword : password; var passwordSecretData = new V1Secret { @@ -1024,21 +1096,22 @@ private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceN } }; _logger.LogDebug("Calling CreateNamespacedSecret()"); - var passwordSecretResponse = Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); + var passwordSecretResponse = + Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); _logger.LogDebug("Finished calling CreateNamespacedSecret()"); _logger.LogDebug("Successfully created secret " + passwordSecretPath); } + break; } } + _logger.LogTrace("Exiting CreateNewSecret()"); return k8SSecretData; - } public V1Secret ReadBuddyPass(string secretName, string passwordSecretPath) { - // Lookup password secret path on cluster to see if it exists _logger.LogDebug("Attempting to lookup password secret path on cluster..."); var splitPasswordPath = passwordSecretPath.Split("/"); @@ -1050,13 +1123,11 @@ public V1Secret ReadBuddyPass(string secretName, string passwordSecretPath) return passwordSecretResponse; } - public V1Secret CreateOrUpdateBuddyPass(string secretName, string passwordFieldName, string passwordSecretPath, string password) + public V1Secret CreateOrUpdateBuddyPass(string secretName, string passwordFieldName, string passwordSecretPath, + string password) { _logger.LogDebug("Adding password secret path to secret..."); - if (string.IsNullOrEmpty(passwordFieldName)) - { - passwordFieldName = "password"; - } + if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password"; // k8SSecretData.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath)); // Lookup password secret path on cluster to see if it exists @@ -1080,7 +1151,8 @@ public V1Secret CreateOrUpdateBuddyPass(string secretName, string passwordFieldN }; try { - var passwordSecretResponse = Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); + var passwordSecretResponse = + Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace); return passwordSecretResponse; } catch (HttpOperationException e) @@ -1088,17 +1160,20 @@ public V1Secret CreateOrUpdateBuddyPass(string secretName, string passwordFieldN _logger.LogError($"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}"); _logger.LogError(e.Message); // Attempt to create a new secret - _logger.LogDebug($"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); + _logger.LogDebug( + $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}"); _logger.LogDebug("Calling CreateNamespacedSecret()"); - var passwordSecretResponse = Client.CoreV1.ReplaceNamespacedSecret(passwordSecretData, secretName, passwordSecretNamespace); + var passwordSecretResponse = + Client.CoreV1.ReplaceNamespacedSecret(passwordSecretData, secretName, passwordSecretNamespace); _logger.LogDebug("Finished calling CreateNamespacedSecret()"); _logger.LogDebug("Successfully created secret " + passwordSecretPath); return passwordSecretResponse; } } - private V1Secret CreateNewSecret(string secretName, string namespaceName, string keyPem, string certPem, List chainPem, string secretType, bool separateChain = false) + private V1Secret CreateNewSecret(string secretName, string namespaceName, string keyPem, string certPem, + List chainPem, string secretType, bool separateChain = false) { _logger.LogTrace("Entered CreateNewSecret()"); _logger.LogDebug("Attempting to create new secret..."); @@ -1139,8 +1214,8 @@ private V1Secret CreateNewSecret(string secretName, string namespaceName, string Data = new Dictionary { - { "tls.key", Encoding.UTF8.GetBytes(keyPem) }, - { "tls.crt", Encoding.UTF8.GetBytes(certPem) } + { "tls.key", Encoding.UTF8.GetBytes(keyPem) }, + { "tls.crt", Encoding.UTF8.GetBytes(certPem) } } }; break; @@ -1151,7 +1226,6 @@ private V1Secret CreateNewSecret(string secretName, string namespaceName, string { Name = secretName, NamespaceProperty = namespaceName - }, Type = "kubernetes.io/tls", @@ -1166,29 +1240,24 @@ private V1Secret CreateNewSecret(string secretName, string namespaceName, string default: throw new NotImplementedException( $"Secret type {secretType} not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}."); - } + if (chainPem is { Count: > 0 }) { var caCert = chainPem.Where(cer => cer != certPem).Aggregate("", (current, cer) => current + cer); if (separateChain) - { - k8SSecretData.Data.Add("ca.crt", Encoding.UTF8.GetBytes(caCert)); - } + k8SSecretData.Data.Add("ca.crt", Encoding.UTF8.GetBytes(caCert)); else - { //update tls.crt w/ full chain k8SSecretData.Data["tls.crt"] = Encoding.UTF8.GetBytes(certPem + caCert); - } - } _logger.LogTrace("Exiting CreateNewSecret()"); return k8SSecretData; - } - private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1Secret existingSecret, V1Secret newSecret) + private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1Secret existingSecret, + V1Secret newSecret) { _logger.LogTrace("Entered UpdateOpaqueSecret()"); @@ -1198,28 +1267,32 @@ private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1S //check if existing secret has ca.crt and if new secret has ca.crt if (existingSecret.Data.ContainsKey("ca.crt") && newSecret.Data.ContainsKey("ca.crt")) { - _logger.LogDebug("Existing secret '{Namespace}/{Name}' has ca.crt adding chain to this field", namespaceName, secretName); + _logger.LogDebug("Existing secret '{Namespace}/{Name}' has ca.crt adding chain to this field", + namespaceName, secretName); _logger.LogTrace("existing ca.crt:\n {CaCrt}", existingSecret.Data["ca.crt"]); existingSecret.Data["ca.crt"] = newSecret.Data["ca.crt"]; _logger.LogTrace("new ca.crt:\n {CaCrt}", newSecret.Data["ca.crt"]); - } else { //Append to tls.crt - _logger.LogDebug("Existing secret '{Namespace}/{Name}' does not have ca.crt, appending to tls.crt", namespaceName, secretName); + _logger.LogDebug("Existing secret '{Namespace}/{Name}' does not have ca.crt, appending to tls.crt", + namespaceName, secretName); if (newSecret.Data.TryGetValue("ca.crt", out var value)) { _logger.LogDebug("Appending ca.crt to tls.crt"); existingSecret.Data["tls.crt"] = - Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"]) + Encoding.UTF8.GetString(value)); + Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"]) + + Encoding.UTF8.GetString(value)); _logger.LogTrace("New tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]); } else { - _logger.LogDebug("No chain was provided, only updating leaf certificate for '{Namespace}/{Name}'", namespaceName, secretName); + _logger.LogDebug("No chain was provided, only updating leaf certificate for '{Namespace}/{Name}'", + namespaceName, secretName); _logger.LogTrace("existing tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]); - existingSecret.Data["tls.crt"] = Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"])); + existingSecret.Data["tls.crt"] = + Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"])); _logger.LogTrace("updated tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]); } } @@ -1232,19 +1305,20 @@ private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1S return secretResponse; } - private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceName, V1Secret existingSecret, string certPem, string keyPem) + private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceName, V1Secret existingSecret, + string certPem, string keyPem) { _logger.LogTrace("Entered UpdateOpaqueSecret()"); var existingCerts = existingSecret.Data.ContainsKey("certificates") ? Encoding.UTF8.GetString(existingSecret.Data["certificates"]) - : ""; + : ""; _logger.LogTrace("Existing certificates: " + existingCerts); var existingKeys = existingSecret.Data.ContainsKey("tls.key") ? Encoding.UTF8.GetString(existingSecret.Data["tls.key"]) - : ""; + : ""; // Logger.LogTrace("Existing private keys: " + existingKeys); if (existingCerts.Contains(certPem) && existingKeys.Contains(keyPem)) @@ -1264,6 +1338,7 @@ private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceN _logger.LogTrace("Adding comma to existing certificates"); newCerts += ","; } + _logger.LogTrace("Adding certificate to existing certificates"); newCerts += certPem; @@ -1280,6 +1355,7 @@ private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceN _logger.LogTrace("Adding comma to existing private keys"); newKeys += ","; } + _logger.LogTrace("Adding private key to existing private keys"); newKeys += keyPem; @@ -1295,7 +1371,8 @@ private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceN return secretResponse; } - private V1Secret UpdateSecretStore(string secretName, string namespaceName, string secretType, string certPem, string keyPem, V1Secret newData, bool append, + private V1Secret UpdateSecretStore(string secretName, string namespaceName, string secretType, string certPem, + string keyPem, V1Secret newData, bool append, bool overwrite = false) { _logger.LogTrace("Entered UpdateSecretStore()"); @@ -1341,12 +1418,14 @@ private V1Secret UpdateSecretStore(string secretName, string namespaceName, stri return secretResponse; } default: - var dErrMsg = $"Secret type not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}."; + var dErrMsg = + $"Secret type not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}."; _logger.LogError(dErrMsg); _logger.LogTrace("Exiting UpdateSecretStore()"); throw new NotImplementedException(dErrMsg); } } + public V1Secret GetCertificateStoreSecret(string secretName, string namespaceName) { _logger.LogTrace("Entered GetCertificateStoreSecret()"); @@ -1370,6 +1449,7 @@ private string CleanOpaqueStore(string existingEntries, string pemString) _logger.LogDebug("Removing leading comma from existing certificates."); existingEntries = existingEntries.Substring(1); } + if (existingEntries.EndsWith(",")) { _logger.LogDebug("Removing trailing comma from existing certificates."); @@ -1381,6 +1461,7 @@ private string CleanOpaqueStore(string existingEntries, string pemString) // Didn't find existing key for whatever reason so no need to delete. _logger.LogWarning("Unable to find existing certificate in opaque secret. No need to remove."); } + _logger.LogTrace("Exiting CleanOpaqueStore()"); return existingEntries; } @@ -1408,11 +1489,11 @@ private V1Secret DeleteCertificateStoreSecret(string secretName, string namespac _logger.LogDebug("Parsing existing certificates from secret into a string."); foreach (var sKey in existingSecret.Data.Keys) { - var existingCerts = Encoding.UTF8.GetString(existingSecret.Data[sKey]); + var existingCerts = Encoding.UTF8.GetString(existingSecret.Data[sKey]); _logger.LogTrace("existingCerts: " + existingCerts); _logger.LogDebug("Parsing existing private keys from secret into a string."); - var existingKeys = Encoding.UTF8.GetString(existingSecret.Data["tls.key"]); + var existingKeys = Encoding.UTF8.GetString(existingSecret.Data["tls.key"]); // Logger.LogTrace("existingKeys: " + existingKeys); _logger.LogDebug("Splitting existing certificates into an array."); @@ -1435,6 +1516,7 @@ private V1Secret DeleteCertificateStoreSecret(string secretName, string namespac _logger.LogDebug("Found empty certificate string. Skipping."); continue; } + _logger.LogDebug("Creating X509Certificate2 from certificate string."); var sCert = new X509Certificate2(); try @@ -1443,7 +1525,8 @@ private V1Secret DeleteCertificateStoreSecret(string secretName, string namespac } catch (Exception e) { - _logger.LogWarning($"Unable to create X509Certificate2 from string in '{sKey}' field. Skipping. Error: {e.Message}"); + _logger.LogWarning( + $"Unable to create X509Certificate2 from string in '{sKey}' field. Skipping. Error: {e.Message}"); continue; } @@ -1465,16 +1548,17 @@ private V1Secret DeleteCertificateStoreSecret(string secretName, string namespac { // Didn't find existing key for whatever reason so no need to delete. // Find the corresponding key the the keys array and by checking if the private key corresponds to the cert public key. - _logger.LogWarning($"Unable to find corresponding private key in opaque secret for certificate {sCert.Thumbprint}. No need to remove."); + _logger.LogWarning( + $"Unable to find corresponding private key in opaque secret for certificate {sCert.Thumbprint}. No need to remove."); } - } + _logger.LogTrace("Incrementing pkey index..."); index++; //Currently keys are assumed to be in the same order as certs. } _logger.LogDebug("Updating existing secret with new certificate data."); - existingSecret.Data[sKey] = Encoding.UTF8.GetBytes(existingCerts); + existingSecret.Data[sKey] = Encoding.UTF8.GetBytes(existingCerts); _logger.LogDebug("Updating existing secret with new key data."); try { @@ -1482,19 +1566,22 @@ private V1Secret DeleteCertificateStoreSecret(string secretName, string namespac } catch (Exception) { - _logger.LogWarning("Unable to update private_keys in opaque secret. This is expected if the secret did not contain private keys to begin with."); - } + _logger.LogWarning( + "Unable to update private_keys in opaque secret. This is expected if the secret did not contain private keys to begin with."); + } // Update Kubernetes secret - _logger.LogDebug($"Updating secret {secretName} in namespace {namespaceName} on {GetHost()} with new certificate data."); + _logger.LogDebug( + $"Updating secret {secretName} in namespace {namespaceName} on {GetHost()} with new certificate data."); _logger.LogTrace("Calling ReplaceNamespacedSecret()"); } return Client.CoreV1.ReplaceNamespacedSecret(existingSecret, secretName, namespaceName); } - public V1Status DeleteCertificateStoreSecret(string secretName, string namespaceName, string storeType, string alias) + public V1Status DeleteCertificateStoreSecret(string secretName, string namespaceName, string storeType, + string alias) { _logger.LogTrace("Entered DeleteCertificateStoreSecret()"); _logger.LogTrace("secretName: " + secretName); @@ -1507,7 +1594,8 @@ public V1Status DeleteCertificateStoreSecret(string secretName, string namespace case "secret": case "opaque": // check the current inventory and only remove the cert if it is found else throw not found exception - _logger.LogDebug($"Attempting to delete certificate from opaque secret {secretName} in namespace {namespaceName} on {GetHost()}"); + _logger.LogDebug( + $"Attempting to delete certificate from opaque secret {secretName} in namespace {namespaceName} on {GetHost()}"); _logger.LogTrace("Calling DeleteCertificateStoreSecret()"); // _ = DeleteCertificateStoreSecret(secretName, namespaceName, alias); return Client.CoreV1.DeleteNamespacedSecret( @@ -1542,6 +1630,7 @@ public V1Status DeleteCertificateStoreSecret(string secretName, string namespace throw new NotImplementedException(dErrMsg); } } + public List DiscoverCertificates() { _logger.LogTrace("Entered DiscoverCertificates()"); @@ -1564,10 +1653,7 @@ public List DiscoverCertificates() ? Encoding.UTF8.GetString(cr.Spec.Request, 0, cr.Spec.Request.Length) : ""; - if (utfCsr != "") - { - _logger.LogTrace("utfCsr: " + utfCsr); - } + if (utfCsr != "") _logger.LogTrace("utfCsr: " + utfCsr); if (utfCert == "") { _logger.LogWarning("CSR has not been signed yet. Skipping."); @@ -1617,6 +1703,7 @@ public X509Certificate ReadDerCertificate(string derString) var certificateParser = new X509CertificateParser(); return certificateParser.ReadCertificate(derData); } + public X509Certificate ReadPemCertificate(string pemString) { using var reader = new StringReader(pemString); @@ -1627,7 +1714,6 @@ public X509Certificate ReadPemCertificate(string pemString) var certificateBytes = pemObject.Content; var certificateParser = new X509CertificateParser(); return certificateParser.ReadCertificate(certificateBytes); - } public string ExtractPrivateKeyAsPem(Pkcs12Store store, string password) @@ -1636,21 +1722,25 @@ public string ExtractPrivateKeyAsPem(Pkcs12Store store, string password) // Get the first private key entry var alias = store.Aliases.FirstOrDefault(entryAlias => store.IsKeyEntry(entryAlias)); - if (alias == null) - { - throw new Exception("No private key found in the provided PFX/P12 file."); - } + if (alias == null) throw new Exception("No private key found in the provided PFX/P12 file."); // Get the private key var keyEntry = store.GetKey(alias); - var privateKeyParams = (RsaPrivateCrtKeyParameters)keyEntry.Key; + var privateKeyParams = keyEntry.Key; + + var pemType = privateKeyParams switch + { + RsaPrivateCrtKeyParameters => "RSA PRIVATE KEY", + ECPrivateKeyParameters => "EC PRIVATE KEY", + _ => throw new Exception("Unsupported private key type.") + }; // Convert the private key to PEM format var sw = new StringWriter(); var pemWriter = new PemWriter(sw); var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParams); var privateKeyBytes = privateKeyInfo.ToAsn1Object().GetEncoded(); - var pemObject = new PemObject("RSA PRIVATE KEY", privateKeyBytes); + var pemObject = new PemObject(pemType, privateKeyBytes); pemWriter.WriteObject(pemObject); pemWriter.Writer.Flush(); @@ -1664,14 +1754,12 @@ public List LoadCertificateChain(string pemData) PemObject pemObject; while ((pemObject = pemReader.ReadPemObject()) != null) - { if (pemObject.Type == "CERTIFICATE") { var certificateParser = new X509CertificateParser(); var certificate = certificateParser.ReadCertificate(pemObject.Content); certificates.Add(certificate); } - } return certificates; } @@ -1684,43 +1772,51 @@ public string ConvertToPem(X509Certificate certificate) pemWriter.WriteObject(pemObject); pemWriter.Writer.Flush(); return stringWriter.ToString(); - } - public List DiscoverSecrets(string[] allowedKeys, string secType, string ns = "default", bool namespaceIsStore = false, bool clusterIsStore = false) + public List DiscoverSecrets(string[] allowedKeys, string secType, string ns = "default", + bool namespaceIsStore = false, bool clusterIsStore = false) { // Get a list of all namespaces _logger.LogTrace("Entered DiscoverSecrets()"); V1NamespaceList namespaces; var clusterName = GetClusterName() ?? GetHost(); + _logger.LogTrace("clusterName: {ClusterName}", clusterName); - var nsList = new string[] { }; + var nsList = Array.Empty(); var locations = new List(); if (secType == "cluster") { - _logger.LogTrace("Discovering K8S cluster secrets from k8s cluster resources and returning only a single location."); + _logger.LogTrace( + "Discovering K8S cluster secrets from k8s cluster resources and returning only a single location"); locations.Add($"{clusterName}"); return locations; } - _logger.LogDebug("Attempting to list k8s namespaces from " + clusterName); + _logger.LogDebug("Attempting to list k8s namespaces from {ClusterName}", clusterName); + _logger.LogTrace("Client BaseUrl: {BaseUrl}", Client.BaseUri); + _logger.LogDebug("Calling CoreV1.ListNamespace()"); namespaces = Client.CoreV1.ListNamespace(); - _logger.LogTrace("namespaces.Items.Count: " + namespaces.Items.Count); - _logger.LogTrace("namespaces.Items: " + namespaces.Items); + + _logger.LogDebug("returned from CoreV1.ListNamespace()"); + _logger.LogTrace("namespaces.Items.Count: {Count}", namespaces.Items.Count); + _logger.LogTrace("namespaces.Items: {Items}", namespaces.Items.ToString()); - nsList = ns.Contains(",") ? ns.Split(",") : new[] { ns }; - foreach (var nsLI in nsList) + nsList = ns.Contains(',') ? ns.Split(",") : new[] { ns }; + foreach (var nsLi in nsList) { + _logger.LogTrace("Iterating through namespace list {NamespaceList}", nsLi); var secretsList = new List(); - _logger.LogTrace("Entering foreach loop to list all secrets in each returned namespace."); + _logger.LogTrace("Entering foreach loop to list all secrets in each returned namespace"); foreach (var nsObj in namespaces.Items) { - if (nsLI != "all" && nsLI != nsObj.Metadata.Name) + if (nsLi != "all" && nsLi != nsObj.Metadata.Name) { - _logger.LogWarning("Skipping namespace " + nsObj.Metadata.Name + " because it does not match the namespace filter."); + _logger.LogWarning( + "Skipping namespace '{Namespace}' because it does not match the namespace filter", nsObj.Metadata.Name); continue; } @@ -1743,7 +1839,6 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string } foreach (var secret in secrets.Items) - { if (secret.Type.ToLower() is "kubernetes.io/tls" or "opaque" or "pkcs12" or "p12" or "pfx" or "jks") { _logger.LogTrace("secret.Type: " + secret.Type); @@ -1759,9 +1854,11 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string case "kubernetes.io/tls": if (secType != "kubernetes.io/tls" && secType != "tls") { - _logger.LogWarning("Skipping secret " + secret.Metadata.Name + " because it is not of type " + secType); + _logger.LogWarning("Skipping secret " + secret.Metadata.Name + + " because it is not of type " + secType); continue; } + _logger.LogDebug("Attempting to parse TLS certificate from secret"); var certData = Encoding.UTF8.GetString(secretData.Data["tls.crt"]); _logger.LogTrace("certData: " + certData); @@ -1770,17 +1867,20 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string var keyData = Encoding.UTF8.GetString(secretData.Data["tls.key"]); _logger.LogDebug("Attempting to convert TLS certificate to X509Certificate2 object"); - // _ = new X509Certificate2(secretData.Data["tls.crt"]); // Check if cert is valid + // _ = new X509Certificate2(secretData.Data["tls.crt"]); // Check if cert is valid var cLocation = $"{clusterName}/{nsObj.Metadata.Name}/secrets/{secret.Metadata.Name}"; - _logger.LogDebug($"Adding certificate location {cLocation} to list of discovered certificates"); + _logger.LogDebug( + $"Adding certificate location {cLocation} to list of discovered certificates"); locations.Add(cLocation); secretsList.Add(certData); break; case "Opaque": - if (secType != "Opaque" && secType != "pkcs12" && secType != "p12" && secType != "pfx" && secType != "jks") + if (secType != "Opaque" && secType != "pkcs12" && secType != "p12" && + secType != "pfx" && secType != "jks") { - _logger.LogWarning("Skipping secret " + secret.Metadata.Name + " because it is not of type " + secType); + _logger.LogWarning("Skipping secret " + secret.Metadata.Name + + " because it is not of type " + secType); continue; } @@ -1788,13 +1888,16 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string _logger.LogDebug("Attempting to parse certificate from opaque secret"); if (secretData.Data == null || secret.Data.Keys == null) { - _logger.LogWarning("secretData.Data is null for secret '" + secret.Metadata.Name + "'. Skipping secret."); + _logger.LogWarning("secretData.Data is null for secret '" + secret.Metadata.Name + + "'. Skipping secret."); continue; } + _logger.LogTrace("Entering foreach loop to check if any allowed keys exist in secret"); foreach (var dataKey in secretData.Data.Keys) { - _logger.LogDebug("Checking if secret key " + dataKey + " is in list of allowed keys" + allowedKeys); + _logger.LogDebug("Checking if secret key " + dataKey + + " is in list of allowed keys" + allowedKeys); _logger.LogTrace("dataKey: " + dataKey); try { @@ -1802,49 +1905,57 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string var dataKeyArray = dataKey.Split("."); var extension = dataKeyArray[^1]; - _logger.LogDebug("Checking if key " + extension + " is in list of allowed keys" + allowedKeys); + _logger.LogDebug("Checking if key " + extension + + " is in list of allowed keys" + allowedKeys); _logger.LogTrace("extension: " + extension); if (!allowedKeys.Contains(extension)) { - _logger.LogTrace("Extension " + extension + " is not in list of allowed keys" + allowedKeys); + _logger.LogTrace("Extension " + extension + + " is not in list of allowed keys" + allowedKeys); if (!allowedKeys.Contains(dataKey)) { - _logger.LogDebug("Skipping secret field" + dataKey + " because it is not in the list of allowed keys" + allowedKeys); + _logger.LogDebug("Skipping secret field" + dataKey + + " because it is not in the list of allowed keys" + + allowedKeys); continue; } } - + _logger.LogDebug("Secret field '" + dataKey + "' is an allowed key."); _logger.LogDebug("Attempting to parse certificate from opaque secret data"); // Attempt to read data as PEM if (secType != "pkcs12" && secType != "jks") { - var certs = Encoding.UTF8.GetString(secretData.Data[dataKey]); _logger.LogTrace("certs: " + certs); var certObj = ReadPemCertificate(certs); if (certObj == null) { - _logger.LogDebug("Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); + _logger.LogDebug( + "Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); // Attempt to read data as DER certObj = ReadDerCertificate(certs); if (certObj == null) { - _logger.LogDebug("Failed to parse certificate from opaque secret data as DER. Skipping secret {Name}", secret?.Metadata?.Name); + _logger.LogDebug( + "Failed to parse certificate from opaque secret data as DER. Skipping secret {Name}", + secret?.Metadata?.Name); continue; } - } + } } else if (secType == "pkcs12" || secType == "jks") { - _logger.LogDebug("Discovery does not support store password for pkcs12 or jks secrets. Assuming secret '{Name}' with matching key '{Key} is valid ", secret?.Metadata?.Name, dataKey); + _logger.LogDebug( + "Discovery does not support store password for pkcs12 or jks secrets. Assuming secret '{Name}' with matching key '{Key} is valid ", + secret?.Metadata?.Name, dataKey); } - - locations.Add($"{clusterName}/{nsObj.Metadata.Name}/secrets/{secret.Metadata.Name}"); + locations.Add( + $"{clusterName}/{nsObj.Metadata.Name}/secrets/{secret.Metadata.Name}"); } catch (Exception e) { @@ -1853,13 +1964,12 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string _logger.LogTrace(e.StackTrace); } } + _logger.LogTrace("Exiting foreach loop to check if any allowed keys exist in secret"); break; } } - } } - } _logger.LogTrace("locations: " + locations); @@ -1867,29 +1977,8 @@ public List DiscoverSecrets(string[] allowedKeys, string secType, string return locations; } - public struct JksSecret - { - public string SecretPath; - public string SecretFieldName; - public V1Secret Secret; - public string Password; - public string PasswordPath; - public List AllowedKeys; - public Dictionary Inventory; - } - - public struct Pkcs12Secret - { - public string SecretPath; - public string SecretFieldName; - public V1Secret Secret; - public string Password; - public string PasswordPath; - public List AllowedKeys; - public Dictionary Inventory; - } - - public JksSecret GetJksSecret(string secretName, string namespaceName, string password = null, string passwordPath = null, List allowedKeys = null) + public JksSecret GetJksSecret(string secretName, string namespaceName, string password = null, + string passwordPath = null, List allowedKeys = null) { _logger.LogTrace("Entered GetJKSSecret()"); _logger.LogTrace("secretName: " + secretName); @@ -1906,18 +1995,15 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa _logger.LogTrace("secret.Data.Keys: {Name}", secret.Data.Keys); _logger.LogTrace("secret.Data.Keys.Count: " + secret.Data.Keys.Count); - allowedKeys ??= new List() { "jks", "JKS", "Jks" }; - + allowedKeys ??= new List { "jks", "JKS", "Jks" }; + var secretData = new Dictionary(); foreach (var secretFieldName in secret?.Data.Keys) { _logger.LogTrace("secretFieldName: {Name}", secretFieldName); var sField = secretFieldName; - if (secretFieldName.Contains('.')) - { - sField = secretFieldName.Split(".")[^1]; - } + if (secretFieldName.Contains('.')) sField = secretFieldName.Split(".")[^1]; var isJksField = allowedKeys.Any(allowedKey => sField.Contains(allowedKey)); if (!isJksField) continue; @@ -1927,7 +2013,8 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa _logger.LogTrace("data: " + data); secretData.Add(secretFieldName, data); } - var output = new JksSecret() + + var output = new JksSecret { Secret = secret, SecretPath = $"{namespaceName}/secrets/{secretName}", @@ -1942,11 +2029,9 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa } throw new InvalidK8SSecretException($"K8S secret {namespaceName}/secrets/{secretName} is empty."); - } catch (HttpOperationException e) { - if (e.Response.StatusCode != HttpStatusCode.NotFound) throw e; // var output = new JksSecret() @@ -1961,13 +2046,16 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa // }; // _logger.LogTrace("Exiting GetJKSSecret()"); // return output; - _logger.LogError("K8S secret {SecretName} not found in namespace {NamespaceName}", secretName, namespaceName); + _logger.LogError("K8S secret {SecretName} not found in namespace {NamespaceName}", secretName, + namespaceName); throw new StoreNotFoundException($"K8S secret not found {namespaceName}/secrets/{secretName}"); } + return new JksSecret(); } - public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, string password = null, string passwordPath = null, List allowedKeys = null) + public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, string password = null, + string passwordPath = null, List allowedKeys = null) { _logger.LogTrace("Entered GetPKCS12Secret()"); _logger.LogTrace("secretName: " + secretName); @@ -1982,7 +2070,7 @@ public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, str _logger.LogTrace("secret.Data.Keys: " + secret.Data.Keys); _logger.LogTrace("secret.Data.Keys.Count: " + secret.Data.Keys.Count); - allowedKeys ??= new List() { "pkcs12", "p12", "P12", "PKCS12", "pfx", "PFX" }; + allowedKeys ??= new List { "pkcs12", "p12", "P12", "PKCS12", "pfx", "PFX" }; var secretData = new Dictionary(); @@ -1991,10 +2079,7 @@ public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, str { _logger.LogTrace("secretFieldName: " + secretFieldName); var sField = secretFieldName; - if (secretFieldName.Contains('.')) - { - sField = secretFieldName.Split(".")[^1]; - } + if (secretFieldName.Contains('.')) sField = secretFieldName.Split(".")[^1]; var isPkcs12Field = allowedKeys.Any(allowedKey => sField.Contains(allowedKey)); if (!isPkcs12Field) continue; @@ -2004,7 +2089,8 @@ public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, str _logger.LogTrace("data: " + data); secretData.Add(secretFieldName, data); } - var output = new Pkcs12Secret() + + var output = new Pkcs12Secret { Secret = secret, SecretPath = $"{namespaceName}/secrets/{secretName}", @@ -2024,6 +2110,7 @@ public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, str throw new StoreNotFoundException($"K8S secret not found {namespaceName}/secrets/{secretName}"); } + return new Pkcs12Secret(); } @@ -2068,7 +2155,7 @@ public CsrObject GenerateCertificateRequest(string name, string[] sans, IPAddres var distinguishedName = new X500DistinguishedName(name); _logger.LogDebug("Generating private key and CSR"); - using var rsa = RSA.Create(4096); + using var rsa = RSA.Create(4096); _logger.LogDebug("Exporting private key and public key"); var pkey = rsa.ExportPkcs8PrivateKey(); @@ -2122,13 +2209,6 @@ public IEnumerable GetCertificateInventory() return inventoryItems; } - public struct CsrObject - { - public string Csr; - public string PrivateKey; - public string PublicKey; - } - public V1Secret CreateOrUpdateJksSecret(JksSecret k8SData, string kubeSecretName, string kubeNamespace) { // Create V1Secret object and replace existing secret @@ -2167,9 +2247,7 @@ public V1Secret CreateOrUpdateJksSecret(JksSecret k8SData, string kubeSecretName catch (HttpOperationException e) { if (e.Response.StatusCode == HttpStatusCode.NotFound) - { return Client.CoreV1.CreateNamespacedSecret(s1, kubeNamespace); - } _logger.LogError("Error checking if secret {Name} exists in namespace {Namespace}: {Message}", kubeSecretName, kubeNamespace, e.Message); } @@ -2196,10 +2274,7 @@ public V1Secret CreateOrUpdatePkcs12Secret(Pkcs12Secret k8SData, string kubeSecr }; s1.Data ??= new Dictionary(); - foreach (var inventoryItem in k8SData.Inventory) - { - s1.Data[inventoryItem.Key] = inventoryItem.Value; - } + foreach (var inventoryItem in k8SData.Inventory) s1.Data[inventoryItem.Key] = inventoryItem.Value; // Create secret if it doesn't exist try @@ -2209,12 +2284,39 @@ public V1Secret CreateOrUpdatePkcs12Secret(Pkcs12Secret k8SData, string kubeSecr catch (HttpOperationException e) { if (e.Response.StatusCode == HttpStatusCode.NotFound) - { return Client.CoreV1.CreateNamespacedSecret(s1, kubeNamespace); - } } // Replace existing secret return Client.CoreV1.ReplaceNamespacedSecret(s1, kubeSecretName, kubeNamespace); } -} + + public struct JksSecret + { + public string SecretPath; + public string SecretFieldName; + public V1Secret Secret; + public string Password; + public string PasswordPath; + public List AllowedKeys; + public Dictionary Inventory; + } + + public struct Pkcs12Secret + { + public string SecretPath; + public string SecretFieldName; + public V1Secret Secret; + public string Password; + public string PasswordPath; + public List AllowedKeys; + public Dictionary Inventory; + } + + public struct CsrObject + { + public string Csr; + public string PrivateKey; + public string PublicKey; + } +} \ No newline at end of file diff --git a/kubernetes-orchestrator-extension/Jobs/Discovery.cs b/kubernetes-orchestrator-extension/Jobs/Discovery.cs index 2ae62e0..8d94260 100644 --- a/kubernetes-orchestrator-extension/Jobs/Discovery.cs +++ b/kubernetes-orchestrator-extension/Jobs/Discovery.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, @@ -11,6 +11,7 @@ using Keyfactor.Extensions.Orchestrator.K8S.Clients; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Logging; using Keyfactor.Orchestrators.Extensions.Interfaces; using Microsoft.Extensions.Logging; @@ -21,8 +22,9 @@ public class Discovery : JobBase, IDiscoveryJobExtension { public Discovery(IPAMSecretResolver resolver) { - Resolver = resolver; + _resolver = resolver; } + //Job Entry Point public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate submitDiscovery) { @@ -39,29 +41,50 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - InitializeStore(config); - Logger.LogInformation("Begin Discovery for K8S Orchestrator Extension for job " + config.JobId); - Logger.LogInformation($"Discovery for store type: {config.Capability}"); + Logger = LogHandler.GetClassLogger(GetType()); + Logger.LogInformation("Begin Discovery for K8S Orchestrator Extension for job {JobID}", config.JobId); + Logger.LogInformation("Discovery for store type: {Capability}", config.Capability); + try + { + Logger.LogDebug("Calling InitializeStore()"); + InitializeStore(config); + Logger.LogDebug("Store initialized successfully"); + } + catch (Exception ex) + { + Logger.LogError("Failed to initialize store: {Error}", ex.Message); + return FailJob("Failed to initialize store: " + ex.Message, config.JobHistoryId); + } + var locations = new List(); KubeSvcCreds = ServerPassword; - KubeClient = new KubeCertificateManagerClient(KubeSvcCreds); - - var namespaces = config.JobProperties["dirs"].ToString()?.Split(','); - if (namespaces is { Length: 0 }) + Logger.LogDebug("Calling KubeCertificateManagerClient()"); + KubeClient = new KubeCertificateManagerClient(KubeSvcCreds, config.UseSSL); //todo does this throw an exception? + Logger.LogDebug("Returned from KubeCertificateManagerClient()"); + if (KubeClient == null) + { + Logger.LogError("Failed to create KubeCertificateManagerClient"); + return FailJob("Failed to create KubeCertificateManagerClient", config.JobHistoryId); + } + + var namespaces = config.JobProperties["dirs"].ToString()?.Split(',') ?? Array.Empty(); + if (namespaces is null or { Length: 0 }) { + Logger.LogDebug("No namespaces provided, using `default` namespace"); namespaces = new[] { "default" }; } - Logger.LogDebug("Namespaces: " + string.Join(",", namespaces)); - var ignoreNamespace = config.JobProperties["ignoreddirs"].ToString()?.Split(','); - Logger.LogDebug("Ignored Namespaces: " + string.Join(",", ignoreNamespace)); + Logger.LogDebug("Namespaces: {Namespaces}", string.Join(",", namespaces)); + + var ignoreNamespace = config.JobProperties["ignoreddirs"].ToString()?.Split(',') ?? Array.Empty(); + Logger.LogDebug("Ignored Namespaces: {Namespaces}", string.Join(",", ignoreNamespace)); - var secretAllowedKeys = config.JobProperties["patterns"].ToString()?.Split(','); - Logger.LogDebug("Secret Allowed Keys: " + string.Join(",", secretAllowedKeys)); + var secretAllowedKeys = config.JobProperties["patterns"].ToString()?.Split(',') ?? Array.Empty(); + Logger.LogDebug("Secret Allowed Keys: {AllowedKeys}", string.Join(",", secretAllowedKeys)); - Logger.LogTrace("Discovery entering switch block based on capability: " + config.Capability); + Logger.LogTrace("Discovery entering switch block based on capability {Capability}", config.Capability); try { //Code logic to: @@ -76,29 +99,46 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd { case "CertStores.K8SCluster.Discovery": // Combine the allowed keys with the default keys - Logger.LogTrace("Entering case: CertStores.K8SCluster.Discovery"); + Logger.LogTrace("Entering case: {Capability}", config.Capability); secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray(); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: tls"); + + Logger.LogInformation( + "Discovering k8s secrets for cluster `{ClusterName}` with allowed keys: `{AllowedKeys}` and secret types: `kubernetes.io/tls, Opaque`", + KubeHost, string.Join(",", secretAllowedKeys)); + Logger.LogDebug("Calling KubeClient.DiscoverSecrets()"); locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "cluster", string.Join(",", namespaces)); + Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()"); + break; case "CertStores.K8SNS.Discovery": // Combine the allowed keys with the default keys - Logger.LogTrace("Entering case: CertStores.K8SNamespace.Discovery"); + Logger.LogTrace("Entering case: {Capability}", config.Capability); secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray(); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: tls"); - locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "namespace", string.Join(",", namespaces)); + Logger.LogInformation( + "Discovering k8s secrets in k8s namespaces `{Namespaces}` with allowed keys: `{AllowedKeys}` and secret types: `kubernetes.io/tls, Opaque`", + string.Join(",", namespaces), string.Join(",", secretAllowedKeys)); + Logger.LogDebug("Calling KubeClient.DiscoverSecrets()"); + locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "namespace", + string.Join(",", namespaces)); + Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()"); break; case "CertStores.K8STLSSecr.Discovery": // Combine the allowed keys with the default keys - Logger.LogTrace("Entering case: CertStores.K8STLSSecr.Discovery"); + Logger.LogTrace("Entering case: {Capability}", config.Capability); secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray(); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: tls"); - locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "kubernetes.io/tls", string.Join(",", namespaces)); + Logger.LogInformation( + "Discovering k8s secrets in k8s namespaces `{Namespaces}` with allowed keys: `{AllowedKeys}` and secret type: `kubernetes.io/tls`", + string.Join(",", namespaces), string.Join(",", secretAllowedKeys)); + Logger.LogDebug("Calling KubeClient.DiscoverSecrets()"); + locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "kubernetes.io/tls", + string.Join(",", namespaces)); + Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()"); break; case "CertStores.K8SSecret.Discovery": - Logger.LogTrace("Entering case: CertStores.K8SSecret.Discovery"); + Logger.LogTrace("Entering case: {Capability}", config.Capability); secretAllowedKeys = secretAllowedKeys.Concat(OpaqueAllowedKeys).ToArray(); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: opaque"); + Logger.LogInformation("Discovering secrets with allowed keys: `{AllowedKeys}` and type: `Opaque`", + string.Join(",", secretAllowedKeys)); locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "Opaque", string.Join(",", namespaces)); break; case "CertStores.K8SPFX.Discovery": @@ -107,72 +147,81 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd // config.JobProperties["extensions"] - Extensions to search // config.JobProperties["ignoreddirs"] - Directories to ignore // config.JobProperties["patterns"] - File name patterns to match - - var pfxNamespaces = config.JobProperties["dirs"].ToString(); - if (pfxNamespaces.Length == 0) - { - pfxNamespaces = "default"; - } + Logger.LogTrace("Entering case: {Capability}", config.Capability); + var secretAllowedKeysStr = config.JobProperties["extensions"].ToString(); var allowedPatterns = config.JobProperties["patterns"].ToString(); - var additionalKeyPatterns = string.IsNullOrEmpty(allowedPatterns) ? new [] {"p12"} : allowedPatterns.Split(','); - secretAllowedKeys = string.IsNullOrEmpty(secretAllowedKeysStr) ? new[] { "p12" } : secretAllowedKeysStr.Split(','); + var additionalKeyPatterns = string.IsNullOrEmpty(allowedPatterns) + ? new[] { "p12" } + : allowedPatterns.Split(','); + secretAllowedKeys = string.IsNullOrEmpty(secretAllowedKeysStr) + ? new[] { "p12" } + : secretAllowedKeysStr.Split(','); - Logger.LogTrace("Entering case: CertStores.K8SCert.Discovery"); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: pkcs12"); - //append pkcs12AllowedKeys to secretAllowedKeys secretAllowedKeys = secretAllowedKeys.Concat(additionalKeyPatterns).ToArray(); secretAllowedKeys = secretAllowedKeys.Concat(Pkcs12AllowedKeys).ToArray(); - + //make secretAllowedKeys unique secretAllowedKeys = secretAllowedKeys.Distinct().ToArray(); - - locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "pkcs12", string.Join(",", pfxNamespaces)); + + Logger.LogInformation("Discovering k8s secrets with allowed keys: `{AllowedKeys}` and type: `pkcs12`", + string.Join(",", secretAllowedKeys)); + Logger.LogDebug("Calling KubeClient.DiscoverSecrets()"); + locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "pkcs12", + string.Join(",", namespaces)); + Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()"); break; case "CertStores.K8SJKS.Discovery": // config.JobProperties["dirs"] - Directories to search // config.JobProperties["extensions"] - Extensions to search // config.JobProperties["ignoreddirs"] - Directories to ignore // config.JobProperties["patterns"] - File name patterns to match - - var jksNamespaces = config.JobProperties["dirs"].ToString(); - if (jksNamespaces.Length == 0) - { - jksNamespaces = "default"; - } + + Logger.LogTrace("Entering case: {Capability}", config.Capability); var jksSecretAllowedKeysStr = config.JobProperties["extensions"].ToString(); var jksAllowedPatterns = config.JobProperties["patterns"].ToString(); - var jksAdditionalKeyPatterns = string.IsNullOrEmpty(jksAllowedPatterns) ? new [] {"jks"} : jksAllowedPatterns.Split(','); - secretAllowedKeys = string.IsNullOrEmpty(jksSecretAllowedKeysStr) ? new[] { "jks" } : jksSecretAllowedKeysStr.Split(','); + var jksAdditionalKeyPatterns = string.IsNullOrEmpty(jksAllowedPatterns) + ? new[] { "jks" } + : jksAllowedPatterns.Split(','); + secretAllowedKeys = string.IsNullOrEmpty(jksSecretAllowedKeysStr) + ? new[] { "jks" } + : jksSecretAllowedKeysStr.Split(','); - Logger.LogTrace("Entering case: CertStores.K8SCert.Discovery"); - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: cert"); - //append pkcs12AllowedKeys to secretAllowedKeys secretAllowedKeys = secretAllowedKeys.Concat(jksAdditionalKeyPatterns).ToArray(); secretAllowedKeys = secretAllowedKeys.Concat(JksAllowedKeys).ToArray(); - + //make secretAllowedKeys unique secretAllowedKeys = secretAllowedKeys.Distinct().ToArray(); - - Logger.LogInformation("Discovering secrets with allowed keys: " + string.Join(",", secretAllowedKeys) + " and type: jks"); - locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "jks", string.Join(",", jksNamespaces)); + + Logger.LogInformation("Discovering k8s secrets with allowed keys: `{AllowedKeys}` and type: `jks`", + string.Join(",", secretAllowedKeys)); + locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "jks", string.Join(",", namespaces)); break; case "CertStores.K8SCert.Discovery": - break; + Logger.LogError("Capability not supported: CertStores.K8SCert.Discovery"); + return FailJob("Discovery not supported for store type `K8SCert`", config.JobHistoryId); } - } catch (Exception ex) { //Status: 2=Success, 3=Warning, 4=Error - Logger.LogError("Discovery job has failed due to an unknown error."); - Logger.LogError(ex.Message); - Logger.LogTrace(ex.StackTrace); - Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job " + config.JobId + " with failure."); + Logger.LogError("Discovery job has failed due to an unknown error"); + Logger.LogError("{Message}", ex.Message); + Logger.LogTrace("{Message}",ex.ToString()); + // iterate through the inner exceptions + var inner = ex.InnerException; + while (inner != null) + { + Logger.LogError("Inner Exception: {Message}", inner.Message); + Logger.LogTrace("{Message}", inner.ToString()); + inner = inner.InnerException; + } + Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job '{JobID}' with failure", + config.JobId); return FailJob(ex.Message, config.JobHistoryId); } @@ -180,8 +229,10 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd { //Sends store locations back to KF command where they can be approved or rejected Logger.LogInformation("Submitting discovered locations to Keyfactor Command..."); - Logger.LogDebug("Discovery locations: " + string.Join(",", locations)); + Logger.LogTrace("Discovery locations: {Locations}", string.Join(",", locations)); + Logger.LogDebug("Calling submitDiscovery.Invoke()"); submitDiscovery.Invoke(locations.Distinct().ToArray()); + Logger.LogDebug("Returned from submitDiscovery.Invoke()"); //Status: 2=Success, 3=Warning, 4=Error return new JobResult { @@ -194,11 +245,18 @@ public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpd { // NOTE: if the cause of the submitInventory.Invoke exception is a communication issue between the Orchestrator server and the Command server, the job status returned here // may not be reflected in Keyfactor Command. - Logger.LogError("Discovery job invoke has failed due to an unknown error." + ex.Message); - Logger.LogTrace(ex.ToString()); - Logger.LogTrace(ex.StackTrace); - Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job " + config.JobId + " with failure."); + Logger.LogError("Discovery job has failed due to an unknown error: `{Error}`", ex.Message); + Logger.LogTrace("{Message}", ex.ToString()); + var inner = ex.InnerException; + while (inner != null) + { + Logger.LogError("Inner Exception: {Message}", inner.Message); + Logger.LogTrace("{Message}", inner.ToString()); + inner = inner.InnerException; + } + Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job '{JobID}' with failure", + config.JobId); return FailJob(ex.Message, config.JobHistoryId); } } -} +} \ No newline at end of file diff --git a/kubernetes-orchestrator-extension/Jobs/Inventory.cs b/kubernetes-orchestrator-extension/Jobs/Inventory.cs index 52796a1..afc3083 100644 --- a/kubernetes-orchestrator-extension/Jobs/Inventory.cs +++ b/kubernetes-orchestrator-extension/Jobs/Inventory.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, @@ -19,7 +19,6 @@ using Microsoft.Extensions.Logging; using Org.BouncyCastle.Pkcs; - namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs; // The Inventory class implements IAgentJobExtension and is meant to find all of the certificates in a given certificate store on a given server @@ -28,8 +27,9 @@ public class Inventory : JobBase, IInventoryJobExtension { public Inventory(IPAMSecretResolver resolver) { - Resolver = resolver; + _resolver = resolver; } + //Job Entry Point public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) { @@ -45,73 +45,86 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd // config.CertificateStoreDetails.Properties - JSON string containing custom store properties for this specific store type //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - - InitializeStore(config); - Logger.LogInformation("Begin INVENTORY for K8S Orchestrator Extension for job " + config.JobId); - Logger.LogInformation($"Inventory for store type: {config.Capability}"); - - Logger.LogDebug($"Server: {KubeClient.GetHost()}"); - Logger.LogDebug($"Store Path: {StorePath}"); - Logger.LogDebug("KubeSecretType: " + KubeSecretType); - Logger.LogDebug("KubeSecretName: " + KubeSecretName); - Logger.LogDebug("KubeNamespace: " + KubeNamespace); - Logger.LogDebug("Host: " + KubeClient.GetHost()); - - Logger.LogTrace("Inventory entering switch based on KubeSecretType: " + KubeSecretType + "..."); try { + InitializeStore(config); + Logger.LogInformation("Begin INVENTORY for K8S Orchestrator Extension for job " + config.JobId); + Logger.LogInformation($"Inventory for store type: {config.Capability}"); + + Logger.LogDebug($"Server: {KubeClient.GetHost()}"); + Logger.LogDebug($"Store Path: {StorePath}"); + Logger.LogDebug("KubeSecretType: " + KubeSecretType); + Logger.LogDebug("KubeSecretName: " + KubeSecretName); + Logger.LogDebug("KubeNamespace: " + KubeNamespace); + Logger.LogDebug("Host: " + KubeClient.GetHost()); + + Logger.LogTrace("Inventory entering switch based on KubeSecretType: " + KubeSecretType + "..."); + var hasPrivateKey = false; Logger.LogTrace("Inventory entering switch based on KubeSecretType: " + KubeSecretType + "..."); - if (Capability.Contains("Cluster")) - { - KubeSecretType = "cluster"; - } - if (Capability.Contains("NS")) - { - KubeSecretType = "namespace"; - } + if (Capability.Contains("Cluster")) KubeSecretType = "cluster"; + if (Capability.Contains("NS")) KubeSecretType = "namespace"; var allowedKeys = new List(); if (!string.IsNullOrEmpty(CertificateDataFieldName)) - { allowedKeys = CertificateDataFieldName.Split(',').ToList(); - } switch (KubeSecretType.ToLower()) { case "secret": case "secrets": case "opaque": - Logger.LogInformation("Inventorying opaque secrets using the following allowed keys: {Keys}", OpaqueAllowedKeys?.ToString()); - try { + Logger.LogInformation("Inventorying opaque secrets using the following allowed keys: {Keys}", + OpaqueAllowedKeys?.ToString()); + try + { var opaqueInventory = HandleTlsSecret(config.JobHistoryId); - Logger.LogDebug("Returned inventory count: {Count}",opaqueInventory.Count.ToString()); + Logger.LogDebug("Returned inventory count: {Count}", opaqueInventory.Count.ToString()); return PushInventory(opaqueInventory, config.JobHistoryId, submitInventory, true); } catch (StoreNotFoundException) { - Logger.LogWarning("Unable to locate Opaque secret {Namespace}/{Name}. Sending empty inventory.", KubeNamespace, KubeSecretName); - return PushInventory(new List() {}, config.JobHistoryId, submitInventory, false, "WARNING: Store not found in Kubernetes cluster. Assuming empty inventory."); + Logger.LogWarning("Unable to locate Opaque secret {Namespace}/{Name}. Sending empty inventory.", + KubeNamespace, KubeSecretName); + return PushInventory(new List(), config.JobHistoryId, submitInventory, false, + "WARNING: Store not found in Kubernetes cluster. Assuming empty inventory."); + } catch (Exception ex) + { + Logger.LogError("Inventory failed with exception: " + ex.Message); + Logger.LogTrace(ex.Message); + Logger.LogTrace(ex.StackTrace); + //Status: 2=Success, 3=Warning, 4=Error + Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId + + " with failure."); + return new JobResult + { + Result = OrchestratorJobStatusJobResult.Failure, + JobHistoryId = config.JobHistoryId, + FailureMessage = ex.Message + }; } - + case "tls_secret": case "tls": case "tlssecret": case "tls_secrets": - Logger.LogInformation("Inventorying TLS secrets using the following allowed keys: {Keys}" , TLSAllowedKeys?.ToString()); + Logger.LogInformation("Inventorying TLS secrets using the following allowed keys: {Keys}", + TLSAllowedKeys?.ToString()); try { var tlsCertsInv = HandleTlsSecret(config.JobHistoryId); - Logger.LogDebug("Returned inventory count: {Count}",tlsCertsInv.Count.ToString()); + Logger.LogDebug("Returned inventory count: {Count}", tlsCertsInv.Count.ToString()); return PushInventory(tlsCertsInv, config.JobHistoryId, submitInventory, true); } catch (StoreNotFoundException ex) { - Logger.LogWarning("Unable to locate tls secret {Namespace}/{Name}. Sending empty inventory.", KubeNamespace, KubeSecretName); - return PushInventory(new List() {}, config.JobHistoryId, submitInventory, false, "WARNING: Store not found in Kubernetes cluster. Assuming empty inventory."); + Logger.LogWarning("Unable to locate tls secret {Namespace}/{Name}. Sending empty inventory.", + KubeNamespace, KubeSecretName); + return PushInventory(new List(), config.JobHistoryId, submitInventory, false, + "WARNING: Store not found in Kubernetes cluster. Assuming empty inventory."); } - + case "certificate": case "cert": case "csr": @@ -127,18 +140,18 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd allowedKeys.AddRange(Pkcs12AllowedKeys); Logger.LogInformation("Inventorying PKCS12 using the following allowed keys: {Keys}", allowedKeys); var pkcs12Inventory = HandlePkcs12Secret(config, allowedKeys); - Logger.LogDebug("Returned inventory count: {Count}",pkcs12Inventory.Count.ToString()); + Logger.LogDebug("Returned inventory count: {Count}", pkcs12Inventory.Count.ToString()); return PushInventory(pkcs12Inventory, config.JobHistoryId, submitInventory, true); case "jks": allowedKeys.AddRange(JksAllowedKeys); Logger.LogInformation("Inventorying JKS using the following allowed keys: {Keys}", allowedKeys); var jksInventory = HandleJKSSecret(config, allowedKeys); - Logger.LogDebug("Returned inventory count: {Count}",jksInventory.Count.ToString()); + Logger.LogDebug("Returned inventory count: {Count}", jksInventory.Count.ToString()); return PushInventory(jksInventory, config.JobHistoryId, submitInventory, true); case "cluster": - var clusterOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", "all", false); - var clusterTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", "all", false); + var clusterOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", "all"); + var clusterTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", "all"); var errors = new List(); var clusterInventoryDict = new Dictionary>(); @@ -149,7 +162,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd KubeSecretType = "secret"; try { - resolveStorePath(opaqueSecret); + ResolveStorePath(opaqueSecret); StorePath = opaqueSecret.Replace("secrets", "secrets/opaque"); //Split storepath by / and remove first 1 elements var storePathSplit = StorePath.Split('/'); @@ -162,10 +175,10 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd } catch (Exception ex) { - Logger.LogError("Error processing TLS Secret: " + opaqueSecret + " - " + ex.Message + "\n\t" + ex.StackTrace); + Logger.LogError("Error processing TLS Secret: " + opaqueSecret + " - " + ex.Message + + "\n\t" + ex.StackTrace); errors.Add(ex.Message); } - } foreach (var tlsSecret in clusterTlsSecrets) @@ -175,7 +188,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd KubeSecretType = "tls_secret"; try { - resolveStorePath(tlsSecret); + ResolveStorePath(tlsSecret); StorePath = tlsSecret.Replace("secrets", "secrets/tls"); //Split storepath by / and remove first 1 elements var storePathSplit = StorePath.Split('/'); @@ -184,23 +197,23 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd StorePath = string.Join("/", storePathSplitList); var tlsObj = HandleTlsSecret(config.JobHistoryId); - clusterInventoryDict[StorePath] = tlsObj; + clusterInventoryDict[StorePath] = tlsObj; } catch (Exception ex) { - Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" + ex.StackTrace); + Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" + + ex.StackTrace); errors.Add(ex.Message); } - } return PushInventory(clusterInventoryDict, config.JobHistoryId, submitInventory, true); case "namespace": - var namespaceOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", KubeNamespace, false); - var namespaceTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", KubeNamespace, false); + var namespaceOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", KubeNamespace); + var namespaceTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", KubeNamespace); var namespaceErrors = new List(); - Dictionary namespaceInventoryDict = new Dictionary(); + var namespaceInventoryDict = new Dictionary(); foreach (var opaqueSecret in namespaceOpaqueSecrets) { KubeSecretName = ""; @@ -208,7 +221,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd KubeSecretType = "secret"; try { - resolveStorePath(opaqueSecret); + ResolveStorePath(opaqueSecret); StorePath = opaqueSecret.Replace("secrets", "secrets/opaque"); //Split storepath by / and remove first 2 elements var storePathSplit = StorePath.Split('/'); @@ -218,14 +231,14 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd StorePath = string.Join("/", storePathSplitList); var opaqueObj = HandleTlsSecret(config.JobHistoryId); - namespaceInventoryDict[StorePath] = opaqueObj[0]; + namespaceInventoryDict[StorePath] = opaqueObj[0]; } catch (Exception ex) { - Logger.LogError("Error processing TLS Secret: " + opaqueSecret + " - " + ex.Message + "\n\t" + ex.StackTrace); + Logger.LogError("Error processing TLS Secret: " + opaqueSecret + " - " + ex.Message + + "\n\t" + ex.StackTrace); namespaceErrors.Add(ex.Message); } - } foreach (var tlsSecret in namespaceTlsSecrets) @@ -235,7 +248,7 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd KubeSecretType = "tls_secret"; try { - resolveStorePath(tlsSecret); + ResolveStorePath(tlsSecret); StorePath = tlsSecret.Replace("secrets", "secrets/tls"); //Split storepath by / and remove first 2 elements @@ -247,14 +260,14 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd var tlsObj = HandleTlsSecret(config.JobHistoryId); - namespaceInventoryDict[StorePath] = tlsObj[0]; + namespaceInventoryDict[StorePath] = tlsObj[0]; } catch (Exception ex) { - Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" + ex.StackTrace); + Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" + + ex.StackTrace); namespaceErrors.Add(ex.Message); } - } return PushInventory(namespaceInventoryDict, config.JobHistoryId, submitInventory, true); @@ -263,7 +276,8 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd Logger.LogError("Inventory failed with exception: " + KubeSecretType + " not supported."); var errorMsg = $"{KubeSecretType} not supported."; Logger.LogError(errorMsg); - Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId + " with failure."); + Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId + + " with failure."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, @@ -278,15 +292,17 @@ public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpd Logger.LogTrace(ex.ToString()); Logger.LogTrace(ex.StackTrace); //Status: 2=Success, 3=Warning, 4=Error - Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId + " with failure."); + Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId + + " with failure."); return new JobResult { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, - FailureMessage = ex.ToString() + FailureMessage = ex.Message }; } } + private Dictionary> HandleJKSSecret(JobConfiguration config, List allowedKeys) { Logger.LogDebug("Enter HandleJKSSecret()"); @@ -294,15 +310,17 @@ private Dictionary> HandleJKSSecret(JobConfiguration config Logger.LogDebug("Attempting to serialize JKS store"); var jksStore = new JksCertificateStoreSerializer(config.JobProperties?.ToString()); //getJksBytesFromKubeSecret - Logger.LogDebug("Attempting to get JKS bytes from K8S secret " + KubeSecretName + " in namespace " + KubeNamespace); - var k8sData = KubeClient.GetJksSecret(KubeSecretName, KubeNamespace, "","", allowedKeys); + Logger.LogDebug("Attempting to get JKS bytes from K8S secret " + KubeSecretName + " in namespace " + + KubeNamespace); + var k8sData = KubeClient.GetJksSecret(KubeSecretName, KubeNamespace, "", "", allowedKeys); var jksInventoryDict = new Dictionary>(); // iterate through the keys in the secret and add them to the jks store Logger.LogDebug("Iterating through keys in K8S secret " + KubeSecretName + " in namespace " + KubeNamespace); foreach (var (keyName, keyBytes) in k8sData.Inventory) { - Logger.LogDebug("Fetching store password for K8S secret " + KubeSecretName + " in namespace " + KubeNamespace + " and key " + keyName); + Logger.LogDebug("Fetching store password for K8S secret " + KubeSecretName + " in namespace " + + KubeNamespace + " and key " + keyName); var keyPassword = getK8SStorePassword(k8sData.Secret); var passwordHash = GetSHA256Hash(keyPassword); Logger.LogTrace("Password hash for '{Secret}/{Key}': {Hash}", KubeSecretName, keyName, passwordHash); @@ -332,13 +350,13 @@ private Dictionary> HandleJKSSecret(JobConfiguration config foreach (var certAlias in jStoreDs.Aliases) { if (certAliasLookup.TryGetValue(certAlias, out var certAliasSubject)) - { if (certAliasSubject == "skip") { - Logger.LogTrace("Certificate alias: {Alias} already exists in lookup with subject '{Subject}'", certAlias, certAliasSubject); - continue; + Logger.LogTrace("Certificate alias: {Alias} already exists in lookup with subject '{Subject}'", + certAlias, certAliasSubject); + continue; } - } + Logger.LogTrace("Certificate alias: {Alias}", certAlias); var certChainList = new List(); @@ -347,7 +365,7 @@ private Dictionary> HandleJKSSecret(JobConfiguration config if (certChain != null) { - certAliasLookup[certAlias] = certChain[0].Certificate.SubjectDN.ToString(); + certAliasLookup[certAlias] = certChain[0].Certificate.SubjectDN.ToString(); if (sourceIsPkcs12 && certChain.Length > 0) { // This is a PKCS12 store that was created as a JKS so we need to check that the aliases aren't the same as the cert chain @@ -359,9 +377,7 @@ private Dictionary> HandleJKSSecret(JobConfiguration config storeAliases.Remove(certAlias); // Iterate though the aliases and add them to the lookup as 'skip' if they are in the chain foreach (var alias in storeAliases.Where(alias => certChainAliases.Contains(alias))) - { certAliasLookup[alias] = "skip"; - } } } else @@ -372,10 +388,7 @@ private Dictionary> HandleJKSSecret(JobConfiguration config var fullAlias = keyAlias + "/" + certAlias; Logger.LogTrace("Full alias: {Alias}", fullAlias); //check if the alias is a private key - if (jStoreDs.IsKeyEntry(certAlias)) - { - hasPrivateKeyJks = true; - } + if (jStoreDs.IsKeyEntry(certAlias)) hasPrivateKeyJks = true; var pKey = jStoreDs.GetKey(certAlias); if (pKey != null) { @@ -388,7 +401,8 @@ private Dictionary> HandleJKSSecret(JobConfiguration config if (certChain != null) { Logger.LogDebug("Certificate chain found for alias '{Alias}'", certAlias); - Logger.LogDebug("Iterating through certificate chain for alias '{Alias}' to build PEM chain", certAlias); + Logger.LogDebug("Iterating through certificate chain for alias '{Alias}' to build PEM chain", + certAlias); foreach (var cert in certChain) { certChainPem = new StringBuilder(); @@ -397,6 +411,7 @@ private Dictionary> HandleJKSSecret(JobConfiguration config certChainPem.AppendLine("-----END CERTIFICATE-----"); certChainList.Add(certChainPem.ToString()); } + Logger.LogTrace("Certificate chain for alias '{Alias}': {Chain}", certAlias, certChainList); } @@ -418,14 +433,12 @@ private Dictionary> HandleJKSSecret(JobConfiguration config certChainPem.AppendLine("-----END CERTIFICATE-----"); certChainList.Add(certChainPem.ToString()); } + Logger.LogDebug("Adding leaf certificate for alias '{Alias}' to inventory", certAlias); - if (certAliasLookup[certAlias] != "skip") - { - jksInventoryDict[fullAlias] = certChainList; - } - + if (certAliasLookup[certAlias] != "skip") jksInventoryDict[fullAlias] = certChainList; } } + return jksInventoryDict; } @@ -475,7 +488,8 @@ private JobResult HandleCertificate(long jobId, SubmitInventoryUpdate submitInve } } - private JobResult PushInventory(IEnumerable certsList, long jobId, SubmitInventoryUpdate submitInventory, bool hasPrivateKey = false, string jobMessage = null) + private JobResult PushInventory(IEnumerable certsList, long jobId, SubmitInventoryUpdate submitInventory, + bool hasPrivateKey = false, string jobMessage = null) { Logger.LogDebug("Entering PushInventory for job id " + jobId + "..."); Logger.LogTrace("submitInventory: " + submitInventory); @@ -488,9 +502,11 @@ private JobResult PushInventory(IEnumerable certsList, long jobId, Submi string alias; if (string.IsNullOrEmpty(cert)) { - Logger.LogWarning($"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); + Logger.LogWarning( + $"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); continue; } + try { Logger.LogDebug("Attempting to load cert as X509Certificate2..."); @@ -507,7 +523,8 @@ private JobResult PushInventory(IEnumerable certsList, long jobId, Submi Logger.LogError(e.Message); Logger.LogTrace(e.ToString()); Logger.LogTrace(e.StackTrace); - Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + jobId + " with failure."); + Logger.LogInformation( + "End INVENTORY for K8S Orchestrator Extension for job " + jobId + " with failure."); return FailJob(e.Message, jobId); } @@ -526,6 +543,7 @@ private JobResult PushInventory(IEnumerable certsList, long jobId, Submi }); break; } + try { Logger.LogDebug("Submitting inventoryItems to Keyfactor Command..."); @@ -548,25 +566,28 @@ private JobResult PushInventory(IEnumerable certsList, long jobId, Submi } } - private JobResult PushInventory(Dictionary certsList, long jobId, SubmitInventoryUpdate submitInventory, bool hasPrivateKey = false) + private JobResult PushInventory(Dictionary certsList, long jobId, + SubmitInventoryUpdate submitInventory, bool hasPrivateKey = false) { Logger.LogDebug("Entering PushInventory for job id " + jobId + "..."); Logger.LogTrace("submitInventory: " + submitInventory); Logger.LogTrace("certsList: " + certsList); var inventoryItems = new List(); - foreach (KeyValuePair certObj in certsList) + foreach (var certObj in certsList) { var cert = certObj.Value; Logger.LogTrace($"Cert:\n{cert}"); // load as x509 - string alias = certObj.Key; + var alias = certObj.Key; Logger.LogDebug("Cert alias: " + alias); if (string.IsNullOrEmpty(cert)) { - Logger.LogWarning($"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); + Logger.LogWarning( + $"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); continue; } + try { Logger.LogDebug("Attempting to load cert as X509Certificate2..."); @@ -574,14 +595,14 @@ private JobResult PushInventory(Dictionary certsList, long jobId ? new X509Certificate2(Encoding.UTF8.GetBytes(cert)) : new X509Certificate2(Convert.FromBase64String(cert)); Logger.LogTrace("Cert loaded as X509Certificate2: " + certFormatted); - } catch (Exception e) { Logger.LogError(e.Message); Logger.LogTrace(e.ToString()); Logger.LogTrace(e.StackTrace); - Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + jobId + " with failure."); + Logger.LogInformation( + "End INVENTORY for K8S Orchestrator Extension for job " + jobId + " with failure."); // return FailJob(e.Message, jobId); } @@ -600,6 +621,7 @@ private JobResult PushInventory(Dictionary certsList, long jobId certs //Array of single X509 certificates in Base64 string format (certificates if chain, single cert if not), something like: }); } + try { Logger.LogDebug("Submitting inventoryItems to Keyfactor Command..."); @@ -622,7 +644,8 @@ private JobResult PushInventory(Dictionary certsList, long jobId } } - private JobResult PushInventory(Dictionary> certsList, long jobId, SubmitInventoryUpdate submitInventory, bool hasPrivateKey = false) + private JobResult PushInventory(Dictionary> certsList, long jobId, + SubmitInventoryUpdate submitInventory, bool hasPrivateKey = false) { Logger.LogDebug("Entering PushInventory for job id " + jobId + "..."); Logger.LogTrace("submitInventory: " + submitInventory); @@ -634,12 +657,13 @@ private JobResult PushInventory(Dictionary> certsList, long // load as x509 - string alias = certObj.Key; + var alias = certObj.Key; Logger.LogDebug("Cert alias: " + alias); if (certs.Count == 0) { - Logger.LogWarning($"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); + Logger.LogWarning( + $"Kubernetes returned an empty inventory for store {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}."); continue; } @@ -657,6 +681,7 @@ private JobResult PushInventory(Dictionary> certsList, long certs //Array of single X509 certificates in Base64 string format (certificates if chain, single cert if not), something like: }); } + try { Logger.LogDebug("Submitting inventoryItems to Keyfactor Command..."); @@ -679,15 +704,13 @@ private JobResult PushInventory(Dictionary> certsList, long } } - private JobResult HandleOpaqueSecret(long jobId, SubmitInventoryUpdate submitInventory, string[] secretManagedKeys, string secretPath = "") + private JobResult HandleOpaqueSecret(long jobId, SubmitInventoryUpdate submitInventory, string[] secretManagedKeys, + string secretPath = "") { Logger.LogDebug("Inventory entering HandleOpaqueSecret for job id " + jobId + "..."); const bool hasPrivateKey = true; //check if secretAllowedKeys is null or empty - if (secretManagedKeys == null || secretManagedKeys.Length == 0) - { - secretManagedKeys = new[] { "certificates" }; - } + if (secretManagedKeys == null || secretManagedKeys.Length == 0) secretManagedKeys = new[] { "certificates" }; Logger.LogTrace("secretManagedKeys: " + secretManagedKeys); Logger.LogDebug( $"Querying Kubernetes secrets of type '{KubeSecretType}' for {KubeSecretName} in namespace {KubeNamespace} on host {KubeClient.GetHost()}..."); @@ -722,9 +745,9 @@ private JobResult HandleOpaqueSecret(long jobId, SubmitInventoryUpdate submitInv Logger.LogTrace("certsList: " + certsList); // certsList.Concat(certificates.Split(CertChainSeparator)); } + Logger.LogInformation("Submitting inventoryItems to Keyfactor Command for job id " + jobId + "..."); return PushInventory(certsList, jobId, submitInventory, hasPrivateKey); - } catch (HttpOperationException e) { @@ -774,13 +797,16 @@ private List HandleTlsSecret(long jobId) Logger.LogTrace("KubeNamespace: " + KubeNamespace); if (KubeNamespace == KubeSecretName) { - Logger.LogWarning("KubeNamespace was equal to KubeSecretName. Setting KubeNamespace to 'default' for job id " + jobId + "..."); + Logger.LogWarning( + "KubeNamespace was equal to KubeSecretName. Setting KubeNamespace to 'default' for job id " + + jobId + "..."); KubeNamespace = "default"; } } else { - Logger.LogWarning("StorePath was null or empty. Setting KubeNamespace to 'default' for job id " + jobId + "..."); + Logger.LogWarning("StorePath was null or empty. Setting KubeNamespace to 'default' for job id " + + jobId + "..."); KubeNamespace = "default"; } } @@ -805,7 +831,7 @@ private List HandleTlsSecret(long jobId) ); Logger.LogDebug("KubeClient.GetCertificateStoreSecret() returned successfully."); Logger.LogTrace("certData: " + certData); - var certificatesBytes = certData.Data["tls.crt"]; + var certificatesBytes = certData.Data["tls.crt"]; Logger.LogTrace("certificatesBytes: " + certificatesBytes); var privateKeyBytes = certData.Data["tls.key"]; byte[] caBytes = null; @@ -816,7 +842,8 @@ private List HandleTlsSecret(long jobId) var certObj = KubeClient.ReadPemCertificate(certPem); if (certObj == null) { - Logger.LogDebug("Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); + Logger.LogDebug( + "Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); // Attempt to read data as DER certObj = KubeClient.ReadDerCertificate(certPem); if (certObj != null) @@ -828,6 +855,7 @@ private List HandleTlsSecret(long jobId) { certPem = KubeClient.ConvertToPem(certObj); } + Logger.LogTrace("certPem: " + certPem); } else @@ -835,10 +863,8 @@ private List HandleTlsSecret(long jobId) certPem = KubeClient.ConvertToPem(certObj); Logger.LogTrace("certPem: " + certPem); } - if (!string.IsNullOrEmpty(certPem)) - { - certsList.Add(certPem); - } + + if (!string.IsNullOrEmpty(certPem)) certsList.Add(certPem); var caPem = ""; if (certData.Data.TryGetValue("ca.crt", out var value)) @@ -848,7 +874,8 @@ private List HandleTlsSecret(long jobId) var caObj = KubeClient.ReadPemCertificate(Encoding.UTF8.GetString(caBytes)); if (caObj == null) { - Logger.LogDebug("Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); + Logger.LogDebug( + "Failed to parse certificate from opaque secret data as PEM. Attempting to parse as DER"); // Attempt to read data as DER caObj = KubeClient.ReadDerCertificate(Encoding.UTF8.GetString(caBytes)); if (caObj != null) @@ -863,10 +890,7 @@ private List HandleTlsSecret(long jobId) } Logger.LogTrace("caPem: " + caPem); - if (!string.IsNullOrEmpty(caPem)) - { - certsList.Add(caPem); - } + if (!string.IsNullOrEmpty(caPem)) certsList.Add(caPem); } else { @@ -883,10 +907,12 @@ private List HandleTlsSecret(long jobId) } } } + // Logger.LogTrace("privateKeyBytes: " + privateKeyBytes); if (privateKeyBytes == null) { - Logger.LogDebug("privateKeyBytes was null. Setting hasPrivateKey to false for job id " + jobId + "..."); + Logger.LogDebug("privateKeyBytes was null. Setting hasPrivateKey to false for job id " + jobId + + "..."); hasPrivateKey = false; } @@ -917,7 +943,7 @@ private List HandleTlsSecret(long jobId) throw new Exception(certDataErrorMsg); } } - + private Dictionary> HandlePkcs12Secret(JobConfiguration config, List allowedKeys) { var hasPrivateKey = false; @@ -937,15 +963,9 @@ private Dictionary> HandlePkcs12Secret(JobConfiguration con var certChainPem = new StringBuilder(); var fullAlias = keyName + "/" + certAlias; //check if the alias is a private key - if (pStoreDs.IsKeyEntry(certAlias)) - { - hasPrivateKey = true; - } + if (pStoreDs.IsKeyEntry(certAlias)) hasPrivateKey = true; var pKey = pStoreDs.GetKey(certAlias); - if (pKey != null) - { - hasPrivateKey = true; - } + if (pKey != null) hasPrivateKey = true; // if (certChain == null) // { @@ -961,7 +981,6 @@ private Dictionary> HandlePkcs12Secret(JobConfiguration con certChainPem.AppendLine(Convert.ToBase64String(cert.Certificate.GetEncoded())); certChainPem.AppendLine("-----END CERTIFICATE-----"); certChainList.Add(certChainPem.ToString()); - } if (certChainList.Count != 0) @@ -988,6 +1007,7 @@ private Dictionary> HandlePkcs12Secret(JobConfiguration con pkcs12InventoryDict[fullAlias] = certChainList; } } + return pkcs12InventoryDict; } -} +} \ No newline at end of file diff --git a/kubernetes-orchestrator-extension/Jobs/JobBase.cs b/kubernetes-orchestrator-extension/Jobs/JobBase.cs index e5e5984..a27262e 100644 --- a/kubernetes-orchestrator-extension/Jobs/JobBase.cs +++ b/kubernetes-orchestrator-extension/Jobs/JobBase.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities.IO.Pem; using Org.BouncyCastle.X509; using PemWriter = Org.BouncyCastle.OpenSsl.PemWriter; @@ -73,8 +74,6 @@ public class K8SJobCertificate public byte[] CertBytes { get; set; } - public string PrivateKeyB64 { get; set; } = ""; - public string PrivateKeyPem { get; set; } = ""; public byte[] PrivateKeyBytes { get; set; } @@ -95,7 +94,6 @@ public class K8SJobCertificate public X509CertificateEntry[] CertificateEntryChain { get; set; } - public byte[] Pkcs12 { get; set; } public List ChainPem { get; set; } @@ -103,34 +101,41 @@ public class K8SJobCertificate public abstract class JobBase { + protected static readonly string[] SupportedKubeStoreTypes; + + private static readonly string[] RequiredProperties; + private const string DefaultPFXSecretFieldName = "pfx"; + private const string DefaultJKSSecretFieldName = "jks"; + private const string DefaultPFXPasswordSecretFieldName = "password"; + + protected static readonly string[] TLSAllowedKeys; + protected static readonly string[] OpaqueAllowedKeys; + protected static readonly string[] CertAllowedKeys; + protected static readonly string[] Pkcs12AllowedKeys; + protected static readonly string[] JksAllowedKeys; + + + protected internal bool SeparateChain { get; set; } = + false; //Don't arbitrarily change this to true without specifying BREAKING CHANGE in the release notes. - static protected readonly string[] SupportedKubeStoreTypes; + protected internal bool IncludeCertChain { get; set; } = + true; //Don't arbitrarily change this to false without specifying BREAKING CHANGE in the release notes. - static protected readonly string[] RequiredProperties; + protected internal string OperationType { get; set; } + protected internal bool SkipTlsValidation { get; set; } = false; - static protected readonly string[] TLSAllowedKeys; - static protected readonly string[] OpaqueAllowedKeys; - static protected readonly string[] CertAllowedKeys; - static protected readonly string[] Pkcs12AllowedKeys; - static protected readonly string[] JksAllowedKeys; - static protected readonly string DefaultPFXSecretFieldName = "pfx"; - static protected readonly string DefaultJKSSecretFieldName = "jks"; - static protected readonly string DefaultPFXPasswordSecretFieldName = "password"; + protected const string CertChainSeparator = ","; - internal protected bool SeparateChain { get; set; } = false; //Don't arbitrarily change this to true without specifying BREAKING CHANGE in the release notes. - internal protected bool IncludeCertChain { get; set; } = true; //Don't arbitrarily change this to false without specifying BREAKING CHANGE in the release notes. - - internal protected string OperationType { get; set; } + protected KubeCertificateManagerClient KubeClient; - static protected string CertChainSeparator = ","; - internal protected KubeCertificateManagerClient KubeClient; + protected ILogger Logger; - internal protected ILogger Logger; static JobBase() { CertAllowedKeys = new[] { "cert", "csr" }; TLSAllowedKeys = new[] { "tls.crt", "tls.key", "ca.crt" }; - OpaqueAllowedKeys = new[] { "tls.crt", "tls.crts", "cert", "certs", "certificate", "certificates", "crt", "crts", "ca.crt" }; + OpaqueAllowedKeys = new[] + { "tls.crt", "tls.crts", "cert", "certs", "certificate", "certificates", "crt", "crts", "ca.crt" }; SupportedKubeStoreTypes = new[] { "secret", "certificate" }; RequiredProperties = new[] { "KubeNamespace", "KubeSecretName", "KubeSecretType" }; Pkcs12AllowedKeys = new[] { "p12", "pkcs12", "pfx" }; @@ -139,107 +144,135 @@ static JobBase() public K8SJobCertificate K8SCertificate { get; set; } - internal protected string Capability { get; set; } + protected internal string Capability { get; set; } - internal protected IPAMSecretResolver Resolver { get; set; } + protected IPAMSecretResolver _resolver; public string StorePath { get; set; } - internal protected string KubeNamespace { get; set; } + protected internal string KubeNamespace { get; set; } - internal protected string KubeSecretName { get; set; } + protected internal string KubeSecretName { get; set; } - internal protected string KubeSecretType { get; set; } + protected internal string KubeSecretType { get; set; } - internal protected string KubeSvcCreds { get; set; } + protected internal string KubeSvcCreds { get; set; } - internal protected string KubeHost { get; set; } + protected internal string KubeHost { get; set; } - internal protected string CertificateDataFieldName { get; set; } + protected internal string CertificateDataFieldName { get; set; } - internal protected string PasswordFieldName { get; set; } + protected internal string PasswordFieldName { get; set; } - internal protected bool PasswordIsSeparateSecret { get; set; } + protected internal bool PasswordIsSeparateSecret { get; set; } - internal protected string StorePasswordPath { get; set; } + protected string StorePasswordPath { get; set; } - internal protected string ServerUsername { get; set; } + private string ServerUsername { get; set; } - internal protected string ServerPassword { get; set; } + protected string ServerPassword { get; set; } - internal protected string StorePassword { get; set; } + protected string StorePassword { get; set; } - internal protected bool Overwrite { get; set; } + protected bool Overwrite { get; set; } - internal protected virtual AsymmetricKeyEntry KeyEntry { get; set; } + protected internal virtual AsymmetricKeyEntry KeyEntry { get; set; } - internal protected ManagementJobConfiguration ManagementConfig { get; set; } + protected internal ManagementJobConfiguration ManagementConfig { get; set; } - internal protected DiscoveryJobConfiguration DiscoveryConfig { get; set; } + protected internal DiscoveryJobConfiguration DiscoveryConfig { get; set; } - internal protected InventoryJobConfiguration InventoryConfig { get; set; } + protected internal InventoryJobConfiguration InventoryConfig { get; set; } public string ExtensionName => "K8S"; public string KubeCluster { get; set; } - + protected void InitializeStore(InventoryJobConfiguration config) { + Logger ??= LogHandler.GetClassLogger(GetType()); + Logger.LogDebug("Entered InitializeStore() for INVENTORY"); InventoryConfig = config; Capability = config.Capability; - Logger = LogHandler.GetClassLogger(GetType()); - Logger.LogTrace("Entered InitializeStore() for INVENTORY."); + Logger.LogTrace("Capability: {Capability}", Capability); + + Logger.LogDebug("Calling JsonConvert.DeserializeObject()"); var props = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); - //var props = Jsonconfig.CertificateStoreDetails.Properties; - ServerUsername = config?.ServerUsername; - ServerPassword = config?.ServerPassword; - StorePassword = config?.CertificateStoreDetails?.StorePassword; + // Logger.LogTrace("Properties: {Properties}", props); // Commented out to avoid logging sensitive information + + ServerUsername = config.ServerUsername; + ServerPassword = config.ServerPassword; + StorePassword = config.CertificateStoreDetails?.StorePassword; StorePath = config.CertificateStoreDetails?.StorePath; // StorePath = GetStorePath(); - Logger.LogTrace($"ServerUsername: {ServerUsername}"); - // Logger.LogTrace($"ServerPassword: {ServerPassword}"); - Logger.LogTrace($"StorePath: {StorePath}"); - Logger.LogTrace("Calling InitializeProperties()"); + Logger.LogTrace("ServerUsername: {ServerUsername}", ServerUsername); + // Logger.LogTrace($"ServerPassword: {ServerPassword}"); // Commented out to avoid logging sensitive information + Logger.LogTrace("StorePath: {StorePath}", StorePath); + Logger.LogDebug("Calling InitializeProperties()"); InitializeProperties(props); - + Logger.LogDebug("Returned from InitializeStore()"); + Logger.LogInformation( + "Initialized Inventory Job Configuration for `{Capability}` with store path `{StorePath}`", Capability, + StorePath); } protected void InitializeStore(DiscoveryJobConfiguration config) { + Logger ??= LogHandler.GetClassLogger(GetType()); + Logger.LogDebug("Entered InitializeStore() for DISCOVERY"); DiscoveryConfig = config; - Logger = LogHandler.GetClassLogger(GetType()); - Logger.LogTrace("Entered InitializeStore() for DISCOVERY."); var props = config.JobProperties; - Capability = config?.Capability; - ServerUsername = config?.ServerUsername; - ServerPassword = config?.ServerPassword; - - Logger.LogTrace($"ServerUsername: {ServerUsername}"); + Capability = config.Capability; + ServerUsername = config.ServerUsername; + ServerPassword = config.ServerPassword; + // check that config has UseSSL bool set + if (config.UseSSL) + { + Logger.LogInformation("UseSSL is set to true, setting k8s client `SkipTlsValidation` to `false`"); + SkipTlsValidation = false; + } + else + { + Logger.LogInformation("UseSSL is set to false, setting k8s client `SkipTlsValidation` to `true`"); + SkipTlsValidation = true; + } - Logger.LogTrace("Calling InitializeProperties()"); + Logger.LogTrace("ServerUsername: {ServerUsername}", ServerUsername); + Logger.LogDebug("Calling InitializeProperties()"); InitializeProperties(props); + Logger.LogDebug("Returned from InitializeStore()"); + Logger.LogInformation( + "Initialized Discovery Job Configuration for `{Capability}` with store path `{StorePath}`", Capability, + StorePath); } protected void InitializeStore(ManagementJobConfiguration config) { + Logger ??= LogHandler.GetClassLogger(GetType()); + Logger.LogDebug("Entered InitializeStore() for MANAGEMENT"); ManagementConfig = config; - Logger = LogHandler.GetClassLogger(GetType()); - Logger.LogTrace("Entered InitializeStore() for MANAGEMENT."); + + Logger.LogDebug("Calling JsonConvert.DeserializeObject()"); var props = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties); - Capability = config?.Capability; - ServerUsername = config?.ServerUsername; - ServerPassword = config?.ServerPassword; + Logger.LogDebug("Returned from JsonConvert.DeserializeObject()"); + Capability = config.Capability; + ServerUsername = config.ServerUsername; + ServerPassword = config.ServerPassword; StorePath = config.CertificateStoreDetails?.StorePath; - Logger.LogTrace($"ServerUsername: {ServerUsername}"); - Logger.LogTrace($"StorePath: {StorePath}"); + Logger.LogTrace("ServerUsername: {ServerUsername}", ServerUsername); + Logger.LogTrace("StorePath: {StorePath}", StorePath); - Logger.LogTrace("Calling InitializeProperties()"); + Logger.LogDebug("Calling InitializeProperties()"); InitializeProperties(props); + Logger.LogDebug("Returned from InitializeProperties()"); // StorePath = config.CertificateStoreDetails?.StorePath; // StorePath = GetStorePath(); Overwrite = config.Overwrite; - Logger.LogTrace($"Overwrite: {Overwrite.ToString()}"); + Logger.LogTrace("Overwrite: {Overwrite}", Overwrite); + Logger.LogInformation( + "Initialized Management Job Configuration for `{Capability}` with store path `{StorePath}`", Capability, + StorePath); } private static string InsertLineBreaks(string input, int lineLength) @@ -252,266 +285,362 @@ private static string InsertLineBreaks(string input, int lineLength) sb.AppendLine(); i += lineLength; } + return sb.ToString(); } - + protected K8SJobCertificate InitJobCertificate(dynamic config) { + Logger ??= LogHandler.GetClassLogger(GetType()); Logger.LogTrace("Entered InitJobCertificate()"); - Logger.LogTrace("Creating new K8SJobCertificate object"); var jobCertObject = new K8SJobCertificate(); - var pKeyPassword = config.JobCertificate.PrivateKeyPassword; - Logger.LogTrace($"pKeyPassword: {pKeyPassword}"); + // Logger.LogTrace($"pKeyPassword: {pKeyPassword}"); // Commented out to avoid logging sensitive information jobCertObject.Password = pKeyPassword; if (!string.IsNullOrEmpty(pKeyPassword)) { - Logger.LogDebug($"Certificate {jobCertObject.CertThumbprint} does not have a password"); + Logger.LogDebug("Certificate {CertThumbprint} does not have a password", jobCertObject.CertThumbprint); Logger.LogTrace("Attempting to create certificate without password"); - byte[] rawData = null; - //Attempt to load as Pkcs12Store if fail then try to load from DER format try { - Pkcs12Store pkcs12Store = LoadPkcs12Store(Convert.FromBase64String(config.JobCertificate.Contents), pKeyPassword); + Logger.LogDebug("Calling LoadPkcs12Store()"); + Pkcs12Store pkcs12Store = LoadPkcs12Store(Convert.FromBase64String(config.JobCertificate.Contents), + pKeyPassword); + Logger.LogDebug("Returned from LoadPkcs12Store()"); - //Get the first certificate from the store + Logger.LogDebug("Attempting to get alias from pkcs12Store"); var alias = pkcs12Store.Aliases.FirstOrDefault(pkcs12Store.IsKeyEntry); + Logger.LogTrace("Alias: {Alias}", alias); + + Logger.LogTrace("Calling pkcs12Store.GetKey() with `{Alias}`", alias); var key = pkcs12Store.GetKey(alias); + Logger.LogTrace("Returned from pkcs12Store.GetKey() with `{Alias}`", alias); - //if if not null then extract the private key unencrypted in PEM format + //if not null then extract the private key unencrypted in PEM format if (key != null) { + Logger.LogDebug("Attempting to extract private key as PEM"); + Logger.LogTrace("Calling ExtractPrivateKeyAsPem()"); var pKeyPem = KubeClient.ExtractPrivateKeyAsPem(pkcs12Store, pKeyPassword); + Logger.LogTrace("Returned from ExtractPrivateKeyAsPem()"); jobCertObject.PrivateKeyPem = pKeyPem; + // Logger.LogTrace("Private key: {PrivateKey}", jobCertObject.PrivateKeyPem); // Commented out to avoid logging sensitive information } + Logger.LogDebug("Attempting to get certificate from pkcs12Store"); + Logger.LogTrace("Calling pkcs12Store.GetCertificate()"); var x509Obj = pkcs12Store.GetCertificate(alias); + Logger.LogTrace("Returned from pkcs12Store.GetCertificate()"); + + Logger.LogDebug("Attempting to get certificate chain from pkcs12Store"); + Logger.LogTrace("Calling pkcs12Store.GetCertificateChain()"); var chain = pkcs12Store.GetCertificateChain(alias); - + Logger.LogTrace("Returned from pkcs12Store.GetCertificateChain()"); + var chainList = chain.Select(c => KubeClient.ConvertToPem(c.Certificate)).ToList(); jobCertObject.CertificateEntry = x509Obj; jobCertObject.CertificateEntryChain = chain; jobCertObject.CertThumbprint = x509Obj.Certificate.Thumbprint(); jobCertObject.ChainPem = chainList; - rawData = Convert.FromBase64String(config.JobCertificate.Contents); jobCertObject.CertPem = KubeClient.ConvertToPem(x509Obj.Certificate); - } catch (Exception e) { - Logger.LogError("Error creating certificate without password, " + e.Message); - Logger.LogTrace(e.StackTrace); - rawData = Convert.FromBase64String(config.JobCertificate.Contents); + Logger.LogError("Error parsing certificate data from pkcs12 format without password: {Error}", + e.Message); + Logger.LogTrace("{Message}", e.StackTrace); jobCertObject.CertThumbprint = config.JobCertificate.Thumbprint; + //todo: should this throw an exception? } } else { pKeyPassword = ""; - // App or Controller certificate, process with X509Certificate2 and Private Key Converter - Logger.LogDebug($"Certificate {jobCertObject.CertThumbprint} does have a password"); - Logger.LogTrace("Attempting to create certificate with password"); + Logger.LogDebug("Certificate {CertThumbprint} does have a password", jobCertObject.CertThumbprint); + Logger.LogTrace("Calling Convert.FromBase64String()"); byte[] certBytes = Convert.FromBase64String(config.JobCertificate.Contents); - Logger.LogTrace("Created certificate with password"); + Logger.LogTrace("Returned from Convert.FromBase64String()"); - Logger.LogDebug($"Attempting to export certificate obj {jobCertObject.CertThumbprint} to raw data"); - //check if certBytes are null or empty if (certBytes.Length == 0) { + Logger.LogError("Certificate `{CertThumbprint}` is empty, this should not happen", + jobCertObject.CertThumbprint); return jobCertObject; } - + + Logger.LogTrace("Calling new X509Certificate2()"); var x509 = new X509Certificate2(certBytes, pKeyPassword); + Logger.LogTrace("Returned from new X509Certificate2()"); - Logger.LogDebug($"Attempting to export certificate obj {jobCertObject.CertThumbprint} to raw data"); + Logger.LogTrace("Calling x509.Export()"); var rawData = x509.Export(X509ContentType.Cert); - Logger.LogTrace($"Exported certificate obj to raw data {rawData}"); + Logger.LogTrace("Returned from x509.Export()"); - Logger.LogDebug("Attempting to create PEM formatted string from raw data for " + jobCertObject.CertThumbprint); - var pemCert = "-----BEGIN CERTIFICATE-----\n" + - Convert.ToBase64String(rawData, Base64FormattingOptions.InsertLineBreaks) + - "\n-----END CERTIFICATE-----"; - Logger.LogTrace($"Created PEM formatted string from raw data\n{pemCert}"); + Logger.LogDebug("Attempting to export certificate `{CertThumbprint}` to PEM format", + jobCertObject.CertThumbprint); + //check if certBytes are null or empty + var pemCert = + "-----BEGIN CERTIFICATE-----\n" + + Convert.ToBase64String(rawData, Base64FormattingOptions.InsertLineBreaks) + + "\n-----END CERTIFICATE-----"; - Logger.LogDebug($"Attempting to create PEM formatted string from PrivateKeyConverter object for {jobCertObject.CertThumbprint}"); - var certB64 = Convert.ToBase64String(x509.RawData); jobCertObject.CertPem = pemCert; jobCertObject.CertBytes = x509.RawData; jobCertObject.CertThumbprint = x509.Thumbprint; jobCertObject.Pkcs12 = certBytes; - PrivateKeyConverter pkey; try { - pkey = PrivateKeyConverterFactory.FromPKCS12(certBytes, pKeyPassword); - // check type of key + Logger.LogDebug("Attempting to export private key for `{CertThumbprint}` to PKCS8", + jobCertObject.CertThumbprint); + Logger.LogTrace("Calling PrivateKeyConverterFactory.FromPKCS12()"); + PrivateKeyConverter pkey = PrivateKeyConverterFactory.FromPKCS12(certBytes, pKeyPassword); + Logger.LogTrace("Returned from PrivateKeyConverterFactory.FromPKCS12()"); + string keyType; - Logger.LogTrace("Checking type of private key"); + Logger.LogTrace("Calling x509.GetRSAPublicKey()"); using (AsymmetricAlgorithm keyAlg = x509.GetRSAPublicKey()) { keyType = keyAlg != null ? "RSA" : "EC"; } - Logger.LogTrace("Private key type is " + keyType); - Logger.LogDebug($"Attempting to export private key for {jobCertObject.CertThumbprint} to PKCS8 blob"); - var pKeyB64 = Convert.ToBase64String(pkey.ToPkcs8BlobUnencrypted(), Base64FormattingOptions.InsertLineBreaks); - jobCertObject.PrivateKeyPem = $"-----BEGIN {keyType} PRIVATE KEY-----\n{pKeyB64}\n-----END {keyType} PRIVATE KEY-----"; - Logger.LogTrace("Private key exported to PKCS8 blob"); + + Logger.LogTrace("Returned from x509.GetRSAPublicKey()"); + + Logger.LogTrace("Private key type is {Type}", keyType); + Logger.LogTrace("Calling pkey.ToPkcs8BlobUnencrypted()"); + var pKeyB64 = Convert.ToBase64String(pkey.ToPkcs8BlobUnencrypted(), + Base64FormattingOptions.InsertLineBreaks); + Logger.LogTrace("Returned from pkey.ToPkcs8BlobUnencrypted()"); + + Logger.LogDebug("Creating private key PEM for `{CertThumbprint}`", jobCertObject.CertThumbprint); + jobCertObject.PrivateKeyPem = + $"-----BEGIN {keyType} PRIVATE KEY-----\n{pKeyB64}\n-----END {keyType} PRIVATE KEY-----"; + // Logger.LogTrace("Private key: {PrivateKey}", jobCertObject.PrivateKeyPem); // Commented out to avoid logging sensitive information + Logger.LogDebug("Private key extracted for `{CertThumbprint}`", jobCertObject.CertThumbprint); } catch (ArgumentException) { - - var refStr = string.IsNullOrEmpty(jobCertObject.Alias) ? jobCertObject.CertThumbprint : jobCertObject.Alias; - - var pkeyErr = "Unable to unpack private key from " + refStr + ", invalid password"; - Logger.LogError(pkeyErr); - // throw new Exception(pkeyErr); + Logger.LogDebug("Private key extraction failed for `{CertThumbprint}`", jobCertObject.CertThumbprint); + var refStr = string.IsNullOrEmpty(jobCertObject.Alias) + ? jobCertObject.CertThumbprint + : jobCertObject.Alias; + + var pkeyErr = $"Unable to unpack private key from `{refStr}`, invalid password"; + Logger.LogError("{Error}", pkeyErr); + // todo: should this throw an exception? } } jobCertObject.StorePassword = config.CertificateStoreDetails.StorePassword; - // Get type of config - Logger.LogTrace("Exiting InitJobCertificate()"); + Logger.LogDebug("Returning from InitJobCertificate()"); return jobCertObject; } - public bool isNamespaceStore(string capability) + private static bool IsNamespaceStore(string capability) { - if (string.IsNullOrEmpty(capability) && capability.Contains("K8SNS")) - { - return true; - } - return false; + return capability != null && string.IsNullOrEmpty(capability) && + capability.Contains("K8SNS", StringComparison.OrdinalIgnoreCase); } - public string resolveStorePath(string spath) + private static bool IsClusterStore(string capability) { - Logger.LogTrace("Entered resolveStorePath()"); - Logger.LogTrace("Passed Store Path: " + spath); + return capability != null && string.IsNullOrEmpty(capability) && + capability.Contains("K8SCLUSTER", StringComparison.OrdinalIgnoreCase); + } - Logger.LogTrace("Attempting to split storepath by '/'"); + protected string ResolveStorePath(string spath) + { + Logger.LogDebug("Entered resolveStorePath()"); + Logger.LogTrace("Store path: {StorePath}", spath); + + Logger.LogTrace("Attempting to split store path by '/'"); var sPathParts = spath.Split("/"); - Logger.LogTrace("Split count: " + sPathParts.Length); - var isNsStore = isNamespaceStore(Capability); + Logger.LogTrace("Split count: {Count}", sPathParts.Length); switch (sPathParts.Length) { - case 1 when Capability.Contains("NS"): - Logger.LogInformation("Store path is 1 part and capability is namespace. Assuming that storepath is namespace and setting KubeSecretName equal empty."); - Logger.LogWarning($"Store is of type namespace. Setting KubeSecretName equal empty and namespace to storepath."); + case 1 when IsNamespaceStore(Capability): + Logger.LogInformation( + "Store is of type `K8SNS` and `StorePath` is length 1; setting `KubeSecretName` to empty and `KubeNamespace` to `StorePath`"); + KubeSecretName = ""; KubeNamespace = sPathParts[0]; break; - case 1 when Capability.Contains("Cluster"): - Logger.LogTrace( - "Store path is 1 part and capability is cluster. Assuming that storepath is the cluster name and setting KubeSecretName and KubeNamespace equal empty."); - Logger.LogWarning($"Store is of type cluster. Setting KubeSecretName and KubeNamespace equal empty."); - KubeSecretName = ""; - KubeNamespace = ""; + case 1 when IsClusterStore(Capability): + Logger.LogInformation( + "Store is of type `K8SCluster` path is 1 part and capability is cluster, assuming that store path is the cluster name and setting 'KubeSecretName' and 'KubeNamespace' equal empty"); + if (!string.IsNullOrEmpty(KubeSecretName)) + { + Logger.LogWarning( + "`KubeSecretName` is not a valid parameter for store type `K8SCluster` and will be set to empty"); + KubeSecretName = ""; + } + + if (!string.IsNullOrEmpty(KubeNamespace)) + { + Logger.LogWarning( + "`KubeNamespace` is not a valid parameter for store type `K8SCluster` and will be set to empty"); + KubeNamespace = ""; + } + break; case 1: - Logger.LogTrace("Store path is 1 part assuming that it is the secret name"); if (string.IsNullOrEmpty(KubeSecretName)) { - Logger.LogTrace("No KubeSecretName set. Setting KubeSecretName to store path."); + Logger.LogInformation( + "`StorePath`: `{StorePath}` is 1 part, assuming that it is the k8s secret name and setting 'KubeSecretName' to `{StorePath}`", + sPathParts[0]); KubeSecretName = sPathParts[0]; } + else + { + Logger.LogInformation( + "`StorePath`: `{StorePath}` is 1 part and `KubeSecretName` is not empty, `StorePath` will be ignored", + spath); + } + break; - case 2 when Capability.Contains("Cluster"): - Logger.LogError("Store path is 2 parts and capability is cluster. This is not a valid combination."); + case 2 when IsClusterStore(Capability): + Logger.LogWarning("`StorePath`: `{StorePath}` is 2 parts this is not a valid combination for `K8SCluster` and will be ignored", spath); break; - case 2 when Capability.Contains("NS"): + case 2 when IsNamespaceStore(Capability): var nsPrefix = sPathParts[0]; + Logger.LogTrace("nsPrefix: {NsPrefix}", nsPrefix); var nsName = sPathParts[1]; - Logger.LogTrace( - "Store path is 2 parts and capability is namespace. Assuming that storepath pattern is either cluster/namespacename or namespace/namespacename"); - Logger.LogDebug("Discarding namespace prefix and setting namespace to storepath"); + Logger.LogTrace("nsName: {NsName}", nsName); + + Logger.LogInformation( + "`StorePath`: `{StorePath}` is 2 parts and store type is `K8SNS`, assuming that store path pattern is either `/` or `namespace/`", spath); if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogTrace("No KubeNamespace set. Setting KubeNamespace to store path."); + Logger.LogInformation("`KubeNamespace` is empty, setting `KubeNamespace` to `{Namespace}`", nsName); KubeNamespace = nsName; } + else + { + Logger.LogInformation("`KubeNamespace` parameter is not empty, ignoring `StorePath` value `{StorePath}`", spath); + } break; case 2: - Logger.LogTrace("Store path is 2 parts assuming that it is the namespace/secret name"); + Logger.LogInformation("`StorePath`: `{StorePath}` is 2 parts, assuming that store path pattern is the `/` ", spath); var kNs = sPathParts[0]; + Logger.LogTrace("kNs: {KubeNamespace}", kNs); var kSn = sPathParts[1]; + Logger.LogTrace("kSn: {KubeSecretName}", kSn); + if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogTrace("No KubeNamespace set. Setting KubeNamespace to store path."); + Logger.LogInformation("`KubeNamespace` is not set, setting `KubeNamespace` to `{Namespace}`", kNs); KubeNamespace = kNs; } + else + { + Logger.LogInformation("`KubeNamespace` is set, ignoring `StorePath` value `{StorePath}`", kNs); + } + if (string.IsNullOrEmpty(KubeSecretName)) { - Logger.LogTrace("No KubeSecretName set. Setting KubeSecretName to store path."); + Logger.LogInformation("`KubeSecretName` is not set, setting `KubeSecretName` to `{Secret}`", kSn); KubeSecretName = kSn; } + else + { + Logger.LogInformation("`KubeSecretName` is set, ignoring `StorePath` value `{StorePath}`", kSn); + } + break; - case 3 when Capability.Contains("Cluster"): - Logger.LogError("Store path is 2 parts and capability is cluster. This is not a valid combination."); + case 3 when IsClusterStore(Capability): + Logger.LogError("`StorePath`: `{StorePath}` is 3 parts and store type is `K8SCluster`, this is not a valid combination and `StorePath` will be ignored", spath); break; - case 3 when Capability.Contains("NS"): - Logger.LogTrace("Store path is 3 parts assuming that storepath pattern is the cluster/namespace/namespacename"); + case 3 when IsNamespaceStore(Capability): + Logger.LogInformation("`StorePath`: `{StorePath}` is 3 parts and store type is `K8SNS`, assuming that store path pattern is `/namespace/`", spath); var nsCluster = sPathParts[0]; + Logger.LogTrace("nsCluster: {NsCluster}", nsCluster); var nsClarifier = sPathParts[1]; + Logger.LogTrace("nsClarifier: {NsClarifier}", nsClarifier); var nsName3 = sPathParts[2]; + Logger.LogTrace("nsName3: {NsName3}", nsName3); if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogTrace("No KubeNamespace set. Setting KubeNamespace to store path."); + Logger.LogInformation("`KubeNamespace` is not set, setting `KubeNamespace` to `{Namespace}`", nsName3); KubeNamespace = nsName3; } - KubeSecretName = ""; + else + { + Logger.LogInformation("`KubeNamespace` is set, ignoring `StorePath` value `{StorePath}`", spath); + } + + if (!string.IsNullOrEmpty(KubeSecretName)) + { + Logger.LogWarning("`KubeSecretName` parameter is not empty, but is not supported for `K8SNS` store type and will be ignored"); + KubeSecretName = ""; + } + break; case 3: - Logger.LogTrace("Store path is 3 parts assuming that it is the cluster/namespace/secret name"); + Logger.LogInformation("Store path is 3 parts assuming that it is the '//`"); var kH = sPathParts[0]; + Logger.LogTrace("kH: {KubeHost}", kH); var kN = sPathParts[1]; + Logger.LogTrace("kN: {KubeNamespace}", kN); var kS = sPathParts[2]; + Logger.LogTrace("kS: {KubeSecretName}", kS); + if (kN is "secret" or "tls" or "certificate" or "namespace") { - Logger.LogTrace("Store path is 3 parts and the second part is a secret type. Assuming that it is the namespace/secret name"); + Logger.LogInformation("Store path is 3 parts and the second part is a reserved keyword, assuming that it is the '//'"); kN = sPathParts[0]; kS = sPathParts[1]; } + if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogTrace("No KubeNamespace set. Setting KubeNamespace to store path."); + Logger.LogTrace("No 'KubeNamespace' set, setting 'KubeNamespace' to store path"); KubeNamespace = kN; } + if (string.IsNullOrEmpty(KubeSecretName)) { - Logger.LogTrace("No KubeSecretName set. Setting KubeSecretName to store path."); + Logger.LogTrace("No 'KubeSecretName' set, setting 'KubeSecretName' to store path"); KubeSecretName = kS; } + break; case 4 when Capability.Contains("Cluster") || Capability.Contains("NS"): - Logger.LogError($"Store path is 4 parts and capability is {Capability}. This is not a valid combination."); + Logger.LogError("Store path is 4 parts and capability is {Capability}. This is not a valid combination", + Capability); break; case 4: - Logger.LogTrace("Store path is 4 parts assuming that it is the cluster/namespace/secret type/secret name"); + Logger.LogTrace( + "Store path is 4 parts assuming that it is the cluster/namespace/secret type/secret name"); var kHN = sPathParts[0]; var kNN = sPathParts[1]; var kST = sPathParts[2]; var kSN = sPathParts[3]; if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogTrace("No KubeNamespace set. Setting KubeNamespace to store path."); + Logger.LogTrace("No 'KubeNamespace' set, setting 'KubeNamespace' to store path"); KubeNamespace = kNN; } + if (string.IsNullOrEmpty(KubeSecretName)) { - Logger.LogTrace("No KubeSecretName set. Setting KubeSecretName to store path."); + Logger.LogTrace("No 'KubeSecretName' set, setting 'KubeSecretName' to store path"); KubeSecretName = kSN; } + break; default: - Logger.LogWarning("Unable to resolve store path. Please check the store path and try again."); + Logger.LogWarning("Unable to resolve store path, please check the store path and try again"); + //todo: does anything need to be handled because of this error? break; } + return GetStorePath(); } @@ -520,13 +649,12 @@ private void InitializeProperties(dynamic storeProperties) Logger.LogTrace("Entered InitializeProperties()"); if (storeProperties == null) throw new ConfigurationException( - $"Invalid configuration. Please provide {RequiredProperties}. Or review the documentation at https://github.com/Keyfactor/kubernetes-orchestrator#custom-fields-tab."); + $"Invalid configuration. Please provide {RequiredProperties}. Or review the documentation at https://github.com/Keyfactor/kubernetes-orchestrator#custom-fields-tab"); // check if key is present and set values if not - try { - Logger.LogDebug("Setting K8S values from store properties."); + Logger.LogDebug("Setting K8S values from store properties"); KubeNamespace = storeProperties["KubeNamespace"]; KubeSecretName = storeProperties["KubeSecretName"]; KubeSecretType = storeProperties["KubeSecretType"]; @@ -539,7 +667,7 @@ private void InitializeProperties(dynamic storeProperties) } else { - Logger.LogDebug("PasswordIsSeparateSecret not found in store properties."); + Logger.LogDebug("PasswordIsSeparateSecret not found in store properties"); PasswordIsSeparateSecret = false; } @@ -550,7 +678,7 @@ private void InitializeProperties(dynamic storeProperties) } else { - Logger.LogDebug("PasswordFieldName not found in store properties."); + Logger.LogDebug("PasswordFieldName not found in store properties"); PasswordFieldName = ""; } @@ -561,7 +689,7 @@ private void InitializeProperties(dynamic storeProperties) } else { - Logger.LogDebug("StorePasswordPath not found in store properties."); + Logger.LogDebug("StorePasswordPath not found in store properties"); StorePasswordPath = ""; } @@ -572,175 +700,221 @@ private void InitializeProperties(dynamic storeProperties) } else { - Logger.LogDebug("KubeSecretKey not found in store properties."); + Logger.LogDebug("KubeSecretKey not found in store properties"); CertificateDataFieldName = ""; } } catch (Exception) { - Logger.LogError("Unknown error while parsing store properties."); - Logger.LogWarning("Setting KubeSecretType and KubeSvcCreds to empty strings."); + Logger.LogError("Unknown error while parsing store properties"); + Logger.LogWarning("Setting KubeSecretType and KubeSvcCreds to empty strings"); KubeSecretType = ""; KubeSvcCreds = ""; } //check if storeProperties contains ServerUsername key - - if (string.IsNullOrEmpty(ServerUsername)) + Logger.LogInformation("Attempting to resolve 'ServerUsername' from store properties or PAM provider"); + var pamServerUsername = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "ServerUsername", ServerUsername); + if (!string.IsNullOrEmpty(pamServerUsername)) { - // check if storeProperties contains ServerUsername ke - Logger.LogDebug("ServerUsername is empty."); - try - { - Logger.LogDebug("Attempting to resolve ServerUsername from store properties or PAM provider. Defaults to 'kubeconfig'."); - ServerUsername = storeProperties.ContainsKey("ServerUsername") && string.IsNullOrEmpty(storeProperties["ServerUsername"]) - ? (string)ResolvePamField("ServerUsername", storeProperties["ServerUsername"]) - : "kubeconfig"; - } - catch (Exception) + Logger.LogInformation( + "ServerUsername resolved from PAM provider, setting 'ServerUsername' to resolved value"); + Logger.LogTrace("PAMServerUsername: {Username}", pamServerUsername); + ServerUsername = pamServerUsername; + } + else + { + Logger.LogInformation( + "ServerUsername not resolved from PAM provider, attempting to resolve 'Server Username' from store properties"); + pamServerUsername = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "Server Username", ServerUsername); + if (!string.IsNullOrEmpty(pamServerUsername)) { - ServerUsername = "kubeconfig"; + Logger.LogInformation( + "ServerUsername resolved from store properties. Setting ServerUsername to resolved value"); + Logger.LogTrace("PAMServerUsername: {Username}", pamServerUsername); + ServerUsername = pamServerUsername; } - Logger.LogTrace("ServerUsername: " + ServerUsername); } - if (string.IsNullOrEmpty(ServerPassword)) + + if (string.IsNullOrEmpty(ServerUsername)) { - Logger.LogDebug("ServerPassword is empty."); - try + Logger.LogInformation("ServerUsername is empty, setting 'ServerUsername' to default value: 'kubeconfig'"); + ServerUsername = "kubeconfig"; + } + + // Check if ServerPassword is empty and resolve from store properties or PAM provider + try + { + Logger.LogInformation("Attempting to resolve 'ServerPassword' from store properties or PAM provider"); + var pamServerPassword = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "ServerPassword", ServerPassword); + if (!string.IsNullOrEmpty(pamServerPassword)) { - Logger.LogDebug("Attempting to resolve ServerPassword from store properties or PAM provider."); - ServerPassword = storeProperties.ContainsKey("ServerPassword") ? (string)ResolvePamField("ServerPassword", storeProperties["ServerPassword"]) : ""; - if (string.IsNullOrEmpty(ServerPassword)) - { - ServerPassword = (string)ResolvePamField("ServerPassword", storeProperties["ServerPassword"]); - } - // Logger.LogTrace("ServerPassword: " + ServerPassword); + Logger.LogInformation( + "ServerPassword resolved from PAM provider, setting 'ServerPassword' to resolved value"); + // Logger.LogTrace("PAMServerPassword: " + pamServerPassword); + ServerPassword = pamServerPassword; } - catch (Exception e) + else { - Logger.LogError("Unable to resolve ServerPassword from store properties or PAM provider, defaulting to empty string."); - ServerPassword = ""; - Logger.LogError(e.Message); - Logger.LogTrace(e.ToString()); - Logger.LogTrace(e.StackTrace); - throw new ConfigurationException("Invalid configuration. ServerPassword not provided or is invalid."); + Logger.LogInformation( + "ServerPassword not resolved from PAM provider, attempting to resolve 'Server Password' from store properties"); + pamServerPassword = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "Server Password", ServerPassword); + if (!string.IsNullOrEmpty(pamServerPassword)) + { + Logger.LogInformation( + "ServerPassword resolved from store properties, setting 'ServerPassword' to resolved value"); + // Logger.LogTrace("PAMServerPassword: " + pamServerPassword); + ServerPassword = pamServerPassword; + } } - } - if (string.IsNullOrEmpty(StorePassword)) + catch (Exception e) { - Logger.LogDebug("StorePassword is empty."); - try + Logger.LogError( + "Unable to resolve 'ServerPassword' from store properties or PAM provider, defaulting to empty string"); + ServerPassword = ""; + Logger.LogError("{Message}", e.Message); + Logger.LogTrace("{Message}", e.ToString()); + Logger.LogTrace("{Trace}", e.StackTrace); + // throw new ConfigurationException("Invalid configuration. ServerPassword not provided or is invalid"); + } + + try + { + Logger.LogInformation("Attempting to resolve 'StorePassword' from store properties or PAM provider"); + var pamStorePassword = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "StorePassword", StorePassword); + if (!string.IsNullOrEmpty(pamStorePassword)) { - Logger.LogDebug("Attempting to resolve StorePassword from store properties or PAM provider."); - StorePassword = storeProperties.ContainsKey("StorePassword") ? (string)ResolvePamField("StorePassword", storeProperties["StorePassword"]) : ""; - if (string.IsNullOrEmpty(ServerPassword)) - { - StorePassword = (string)ResolvePamField("StorePassword", storeProperties["StorePassword"]); - } - // Logger.LogTrace("StorePassword: " + StorePassword); + Logger.LogInformation( + "StorePassword resolved from PAM provider, setting 'StorePassword' to resolved value"); + StorePassword = pamStorePassword; } - catch (Exception e) + else { - Logger.LogError("Unable to resolve StorePassword from store properties or PAM provider, defaulting to empty string."); - ServerPassword = ""; - Logger.LogError(e.Message); - Logger.LogTrace(e.ToString()); - Logger.LogTrace(e.StackTrace); - throw new ConfigurationException("Invalid configuration. ServerPassword not provided or is invalid."); + Logger.LogInformation( + "StorePassword not resolved from PAM provider, attempting to resolve 'Store Password' from store properties"); + pamStorePassword = + (string)PAMUtilities.ResolvePAMField(_resolver, Logger, "Store Password", StorePassword); + if (!string.IsNullOrEmpty(pamStorePassword)) + { + Logger.LogInformation( + "StorePassword resolved from store properties, setting 'StorePassword' to resolved value"); + StorePassword = pamStorePassword; + } } - } - // var storePassword = ResolvePamField("Store Password", storeProperties.CertificateStoreDetails.StorePassword); - // - // if (storePassword != null) - // { - // // Logger.LogWarning($"Store password provided but is not supported by store type {storeProperties.Capability})."); - // storeProperties["StorePassword"] = storePassword; - // } + catch (Exception e) + { + Logger.LogError( + "Unable to resolve 'StorePassword' from store properties or PAM provider, defaulting to empty string"); + StorePassword = ""; + Logger.LogError("{Message}", e.Message); + Logger.LogTrace("{Message}", e.ToString()); + Logger.LogTrace("{Trace}", e.StackTrace); + // throw new ConfigurationException("Invalid configuration. StorePassword not provided or is invalid"); + } if (ServerUsername == "kubeconfig" || string.IsNullOrEmpty(ServerUsername)) { Logger.LogInformation("Using kubeconfig provided by 'Server Password' field"); storeProperties["KubeSvcCreds"] = ServerPassword; KubeSvcCreds = ServerPassword; - // logger.LogTrace($"KubeSvcCreds: {localCertStore.KubeSvcCreds}"); //Do not log passwords } - // if (string.IsNullOrEmpty(KubeSvcCreds)) - // { - // const string credsErr = - // "No credentials provided to connect to Kubernetes. Please provide a kubeconfig file. See https://github.com/Keyfactor/kubernetes-orchestrator/blob/main/scripts/kubernetes/get_service_account_creds.sh"; - // Logger.LogError(credsErr); - // throw new AuthenticationException(credsErr); - // } - switch (KubeSecretType) { case "pfx": case "p12": case "pkcs12": - PasswordFieldName = storeProperties.ContainsKey("PasswordFieldName") ? storeProperties["PasswordFieldName"] : DefaultPFXPasswordSecretFieldName; - PasswordIsSeparateSecret = storeProperties.ContainsKey("PasswordIsSeparateSecret") ? storeProperties["PasswordIsSeparateSecret"] : false; - StorePasswordPath = storeProperties.ContainsKey("StorePasswordPath") ? storeProperties["StorePasswordPath"] : ""; - PasswordIsK8SSecret = storeProperties.ContainsKey("PasswordIsK8SSecret") ? storeProperties["PasswordIsK8SSecret"] : false; - KubeSecretPassword = storeProperties.ContainsKey("KubeSecretPassword") ? storeProperties["KubeSecretPassword"] : ""; - CertificateDataFieldName = storeProperties.ContainsKey("CertificateDataFieldName") ? storeProperties["CertificateDataFieldName"] : DefaultPFXSecretFieldName; + Logger.LogInformation( + "Kubernetes certificate store type is 'pfx'. Setting default values for 'PasswordFieldName' and 'CertificateDataFieldName'"); + PasswordFieldName = storeProperties.ContainsKey("PasswordFieldName") + ? storeProperties["PasswordFieldName"] + : DefaultPFXPasswordSecretFieldName; + PasswordIsSeparateSecret = storeProperties.ContainsKey("PasswordIsSeparateSecret") + ? storeProperties["PasswordIsSeparateSecret"] + : false; + StorePasswordPath = storeProperties.ContainsKey("StorePasswordPath") + ? storeProperties["StorePasswordPath"] + : ""; + PasswordIsK8SSecret = storeProperties.ContainsKey("PasswordIsK8SSecret") + ? storeProperties["PasswordIsK8SSecret"] + : false; + KubeSecretPassword = storeProperties.ContainsKey("KubeSecretPassword") + ? storeProperties["KubeSecretPassword"] + : ""; + CertificateDataFieldName = storeProperties.ContainsKey("CertificateDataFieldName") + ? storeProperties["CertificateDataFieldName"] + : DefaultPFXSecretFieldName; break; case "jks": - PasswordFieldName = storeProperties.ContainsKey("PasswordFieldName") ? storeProperties["PasswordFieldName"] : DefaultPFXPasswordSecretFieldName; - PasswordIsSeparateSecret = storeProperties.ContainsKey("PasswordIsSeparateSecret") ? bool.Parse(storeProperties["PasswordIsSeparateSecret"]) : false; - StorePasswordPath = storeProperties.ContainsKey("StorePasswordPath") ? storeProperties["StorePasswordPath"] : ""; - PasswordIsK8SSecret = storeProperties.ContainsKey("PasswordIsK8SSecret") ? storeProperties["PasswordIsK8SSecret"] : false; - KubeSecretPassword = storeProperties.ContainsKey("KubeSecretPassword") ? storeProperties["KubeSecretPassword"] : ""; - CertificateDataFieldName = storeProperties.ContainsKey("CertificateDataFieldName") ? storeProperties["CertificateDataFieldName"] : DefaultJKSSecretFieldName; - break; - - PasswordFieldName = storeProperties.ContainsKey("PasswordFieldName") ? storeProperties["PasswordFieldName"] : DefaultPFXPasswordSecretFieldName; - PasswordIsSeparateSecret = storeProperties.ContainsKey("PasswordIsSeparateSecret") ? bool.Parse(storeProperties["PasswordIsSeparateSecret"]) : false; - StorePasswordPath = storeProperties.ContainsKey("StorePasswordPath") ? storeProperties["StorePasswordPath"] : ""; - PasswordIsK8SSecret = storeProperties.ContainsKey("PasswordIsK8SSecret") ? bool.Parse(storeProperties["PasswordIsK8SSecret"]) : false; - KubeSecretPassword = storeProperties.ContainsKey("KubeSecretPassword") ? storeProperties["KubeSecretPassword"] : ""; - CertificateDataFieldName = storeProperties.ContainsKey("KubeSecretKey") ? storeProperties["KubeSecretKey"] : DefaultPFXSecretFieldName; + Logger.LogInformation( + "Kubernetes certificate store type is 'jks'. Setting default values for 'PasswordFieldName' and 'CertificateDataFieldName'"); + PasswordFieldName = storeProperties.ContainsKey("PasswordFieldName") + ? storeProperties["PasswordFieldName"] + : DefaultPFXPasswordSecretFieldName; + PasswordIsSeparateSecret = storeProperties.ContainsKey("PasswordIsSeparateSecret") + ? bool.Parse(storeProperties["PasswordIsSeparateSecret"]) + : false; + StorePasswordPath = storeProperties.ContainsKey("StorePasswordPath") + ? storeProperties["StorePasswordPath"] + : ""; + PasswordIsK8SSecret = storeProperties.ContainsKey("PasswordIsK8SSecret") + ? storeProperties["PasswordIsK8SSecret"] + : false; + KubeSecretPassword = storeProperties.ContainsKey("KubeSecretPassword") + ? storeProperties["KubeSecretPassword"] + : ""; + CertificateDataFieldName = storeProperties.ContainsKey("CertificateDataFieldName") + ? storeProperties["CertificateDataFieldName"] + : DefaultJKSSecretFieldName; break; - } - KubeClient = new KubeCertificateManagerClient(KubeSvcCreds); - - KubeHost = KubeClient.GetHost(); - KubeCluster = KubeClient.GetClusterName(); + Logger.LogTrace("Creating new KubeCertificateManagerClient object"); + // KubeClient = new KubeCertificateManagerClient(KubeSvcCreds); + // + // Logger.LogTrace("Getting KubeHost and KubeCluster from KubeClient"); + // KubeHost = KubeClient.GetHost(); + // Logger.LogTrace("KubeHost: {KubeHost}", KubeHost); + // + // Logger.LogTrace("Getting cluster name from KubeClient"); + // KubeCluster = KubeClient.GetClusterName(); + // Logger.LogTrace("KubeCluster: {KubeCluster}", KubeCluster); - if (string.IsNullOrEmpty(KubeSecretName) && !string.IsNullOrEmpty(StorePath) && !Capability.Contains("NS") && !Capability.Contains("Cluster")) + if (string.IsNullOrEmpty(KubeSecretName) && !string.IsNullOrEmpty(StorePath) && !Capability.Contains("NS") && + !Capability.Contains("Cluster")) { - Logger.LogDebug("KubeSecretName is empty. Attempting to set KubeSecretName from StorePath."); - resolveStorePath(StorePath); + Logger.LogDebug("KubeSecretName is empty, attempting to set 'KubeSecretName' from StorePath"); + ResolveStorePath(StorePath); } if (string.IsNullOrEmpty(KubeNamespace) && !string.IsNullOrEmpty(StorePath)) { - Logger.LogDebug("KubeNamespace is empty. Attempting to set KubeNamespace from StorePath."); - resolveStorePath(StorePath); + Logger.LogDebug("KubeNamespace is empty, attempting to set 'KubeNamespace' from StorePath"); + ResolveStorePath(StorePath); } if (string.IsNullOrEmpty(KubeNamespace)) { - Logger.LogDebug("KubeNamespace is empty. Setting KubeNamespace to 'default'."); + Logger.LogDebug("KubeNamespace is empty, setting 'KubeNamespace' to 'default'"); KubeNamespace = "default"; } - Logger.LogDebug($"KubeNamespace: {KubeNamespace}"); - Logger.LogDebug($"KubeSecretName: {KubeSecretName}"); - Logger.LogDebug($"KubeSecretType: {KubeSecretType}"); - - if (string.IsNullOrEmpty(KubeSecretName)) - { - // KubeSecretName = StorePath.Split("/").Last(); - Logger.LogWarning("KubeSecretName is empty. Setting KubeSecretName to StorePath."); - KubeSecretName = StorePath; - Logger.LogTrace("KubeSecretName: " + KubeSecretName); - } + Logger.LogDebug("KubeNamespace: {KubeNamespace}", KubeNamespace); + Logger.LogDebug("KubeSecretName: {KubeSecretName}", KubeSecretName); + Logger.LogDebug("KubeSecretType: {KubeSecretType}", KubeSecretName); + if (!string.IsNullOrEmpty(KubeSecretName)) return; + // KubeSecretName = StorePath.Split("/").Last(); + Logger.LogWarning("KubeSecretName is empty, setting 'KubeSecretName' to StorePath"); + KubeSecretName = StorePath; + Logger.LogTrace("KubeSecretName: {KubeSecretName}", KubeSecretName); } public bool PasswordIsK8SSecret { get; set; } = false; @@ -769,62 +943,62 @@ public string GetStorePath() secretType = KubeSecretType.ToLower(); } - Logger.LogTrace("secretType: " + secretType); - Logger.LogTrace("Entered switch statement based on secretType."); + Logger.LogTrace("secretType: {SecretType}", secretType); + Logger.LogTrace("Entered switch statement based on secretType"); switch (secretType) { case "secret": case "opaque": case "tls": case "tls_secret": - Logger.LogDebug("Kubernetes secret resource type. Setting secretType to 'secret'."); + Logger.LogDebug("Kubernetes secret resource type, setting secretType to 'secret'"); secretType = "secret"; break; case "cert": case "certs": case "certificate": case "certificates": - Logger.LogDebug("Kubernetes certificate resource type. Setting secretType to 'certificate'."); + Logger.LogDebug("Kubernetes certificate resource type, setting secretType to 'certificate'"); secretType = "certificate"; break; case "namespace": - storePath = $"{KubeClient.GetClusterName()}/namespace/{KubeNamespace}"; + Logger.LogDebug("Kubernetes namespace resource type, setting secretType to 'namespace'"); KubeSecretType = "namespace"; + + Logger.LogDebug( + "Setting store path to 'cluster/namespace/namespacename' for 'namespace' secret type"); + storePath = $"{KubeClient.GetClusterName()}/namespace/{KubeNamespace}"; + Logger.LogDebug("Returning storePath: {StorePath}", storePath); return storePath; case "cluster": + Logger.LogDebug("Kubernetes cluster resource type, setting secretType to 'cluster'"); KubeSecretType = "cluster"; + Logger.LogDebug("Returning storePath: {StorePath}", storePath); return storePath; default: - Logger.LogWarning("Unknown secret type. Will use value provided."); - Logger.LogTrace($"secretType: {secretType}"); + Logger.LogWarning("Unknown secret type '{SecretType}' will use value provided", secretType); + Logger.LogTrace("secretType: {SecretType}", secretType); break; } - Logger.LogTrace("Building StorePath."); + Logger.LogDebug("Building StorePath"); storePath = $"{KubeClient.GetClusterName()}/{KubeNamespace}/{secretType}/{KubeSecretName}"; - Logger.LogDebug("Returning StorePath: " + storePath); + Logger.LogDebug("Returning storePath: {StorePath}", storePath); return storePath; } catch (Exception e) { - Logger.LogError("Unknown error constructing canonical store path."); + Logger.LogError("Unknown error constructing canonical store path {Error}", e.Message); return StorePath; } - - } - - protected string ResolvePamField(string name, string value) - { - Logger.LogTrace($"Attempting to resolved PAM eligible field {name}"); - return Resolver.Resolve(name); } protected byte[] GetKeyBytes(X509Certificate2 certObj, string certPassword = null) { - Logger.LogTrace("Entered GetKeyBytes()"); - Logger.LogTrace("Key algo: " + certObj.GetKeyAlgorithm()); - Logger.LogTrace("Has private key: " + certObj.HasPrivateKey); - Logger.LogTrace("Pub key: " + certObj.GetPublicKey()); + Logger.LogDebug("Entered GetKeyBytes()"); + Logger.LogTrace("Key algo: {KeyAlgo}", certObj.GetKeyAlgorithm()); + Logger.LogTrace("Has private key: {HasPrivateKey}", certObj.HasPrivateKey); + Logger.LogTrace("Pub key: {PublicKey}", certObj.GetPublicKey()); byte[] keyBytes; @@ -851,32 +1025,32 @@ protected byte[] GetKeyBytes(X509Certificate2 certObj, string certPassword = nul Logger.LogTrace("GetDSAPrivateKey().ExportPkcs8PrivateKey(): completed"); break; default: - Logger.LogWarning("Unknown key algorithm. Attempting to export as PKCS12."); + Logger.LogWarning("Unknown key algorithm, attempting to export as PKCS12"); Logger.LogTrace("Export(X509ContentType.Pkcs12, certPassword)"); keyBytes = certObj.Export(X509ContentType.Pkcs12, certPassword); Logger.LogTrace("Export(X509ContentType.Pkcs12, certPassword) complete"); break; } + if (keyBytes != null) return keyBytes; - Logger.LogError("Key bytes are null. This is unexpected."); - throw new Exception("Key bytes are null. This is unexpected."); + Logger.LogError("Unable to parse private key"); - return keyBytes; + throw new InvalidKeyException($"Unable to parse private key from certificate '{certObj.Thumbprint}'"); } catch (Exception e) { - Logger.LogError("Unknown error getting key bytes, but we're going to try a different method."); - Logger.LogError(e.Message); - Logger.LogTrace(e.ToString()); - Logger.LogTrace(e.StackTrace); + Logger.LogError("Unknown error getting key bytes, but we're going to try a different method"); + Logger.LogError("{Message}", e.Message); + Logger.LogTrace("{Message}", e.ToString()); + Logger.LogTrace("{Trace}", e.StackTrace); try { if (certObj.HasPrivateKey) { try { - Logger.LogDebug("Attempting to export private key as PKCS8."); + Logger.LogDebug("Attempting to export private key as PKCS8"); Logger.LogTrace("ExportPkcs8PrivateKey()"); keyBytes = certObj.PrivateKey.ExportPkcs8PrivateKey(); Logger.LogTrace("ExportPkcs8PrivateKey() complete"); @@ -886,12 +1060,13 @@ protected byte[] GetKeyBytes(X509Certificate2 certObj, string certPassword = nul } catch (Exception e2) { - Logger.LogError("Unknown error exporting private key as PKCS8, but we're going to try a a final method ."); + Logger.LogError( + "Unknown error exporting private key as PKCS8, but we're going to try a a final method "); Logger.LogError(e2.Message); Logger.LogTrace(e2.ToString()); Logger.LogTrace(e2.StackTrace); //attempt to export encrypted pkcs8 - Logger.LogDebug("Attempting to export encrypted PKCS8 private key."); + Logger.LogDebug("Attempting to export encrypted PKCS8 private key"); Logger.LogTrace("ExportEncryptedPkcs8PrivateKey()"); keyBytes = certObj.PrivateKey.ExportEncryptedPkcs8PrivateKey(certPassword, new PbeParameters( @@ -901,21 +1076,21 @@ protected byte[] GetKeyBytes(X509Certificate2 certObj, string certPassword = nul Logger.LogTrace("ExportEncryptedPkcs8PrivateKey() complete"); return keyBytes; } - } } catch (Exception ie) { - Logger.LogError("Unknown error exporting private key as PKCS8, returning null."); - Logger.LogError(ie.Message); - Logger.LogTrace(ie.ToString()); - Logger.LogTrace(ie.StackTrace); + Logger.LogError("Unknown error exporting private key as PKCS8, returning null"); + Logger.LogError("{Message}", ie.Message); + Logger.LogTrace("{Message}", ie.ToString()); + Logger.LogTrace("{Trace}", ie.StackTrace); } - return new byte[] { }; + + return Array.Empty(); } } - static protected JobResult FailJob(string message, long jobHistoryId) + protected static JobResult FailJob(string message, long jobHistoryId) { return new JobResult { @@ -925,13 +1100,12 @@ static protected JobResult FailJob(string message, long jobHistoryId) }; } - static protected JobResult SuccessJob(long jobHistoryId, string jobMessage = null) + protected static JobResult SuccessJob(long jobHistoryId, string jobMessage = null) { var result = new JobResult { Result = OrchestratorJobStatusJobResult.Success, JobHistoryId = jobHistoryId, - }; if (!string.IsNullOrEmpty(jobMessage)) @@ -948,72 +1122,72 @@ protected string ParseJobPrivateKey(ManagementJobConfiguration config) if (string.IsNullOrWhiteSpace(config.JobCertificate.Alias)) Logger.LogTrace("No Alias Found"); // Load PFX - Logger.LogTrace("Loading PFX..."); + Logger.LogTrace("Loading PFX from job contents"); var pfxBytes = Convert.FromBase64String(config.JobCertificate.Contents); - Logger.LogTrace("Loaded PFX..."); - Pkcs12Store p; - string alias = config.JobCertificate.Alias; + Logger.LogTrace("PFX loaded successfully"); + + var alias = config.JobCertificate.Alias; + Logger.LogTrace("Alias: {Alias}", alias); - Logger.LogTrace("Creating Pkcs12Store..."); + Logger.LogTrace("Creating Pkcs12Store object"); // Load the PKCS12 bytes into a Pkcs12Store object - using (MemoryStream pkcs12Stream = new MemoryStream(pfxBytes)) - { - Pkcs12Store store = new Pkcs12StoreBuilder().Build(); + using var pkcs12Stream = new MemoryStream(pfxBytes); + var store = new Pkcs12StoreBuilder().Build(); - store.Load(pkcs12Stream, config.JobCertificate.PrivateKeyPassword.ToCharArray()); + Logger.LogDebug("Attempting to load PFX into store using password"); + store.Load(pkcs12Stream, config.JobCertificate.PrivateKeyPassword.ToCharArray()); - // Find the private key entry with the given alias - foreach (string aliasName in store.Aliases) - { - if (aliasName.Equals(alias) && store.IsKeyEntry(aliasName)) - { - AsymmetricKeyEntry keyEntry = store.GetKey(aliasName); + // Find the private key entry with the given alias + Logger.LogDebug("Attempting to get private key entry with alias"); + foreach (var aliasName in store.Aliases) + { + Logger.LogTrace("Alias: {Alias}", aliasName); + if (!aliasName.Equals(alias) || !store.IsKeyEntry(aliasName)) continue; + Logger.LogDebug("Alias found, attempting to get private key"); + var keyEntry = store.GetKey(aliasName); - // Convert the private key to unencrypted PEM format - using (StringWriter stringWriter = new StringWriter()) - { - PemWriter pemWriter = new PemWriter(stringWriter); - pemWriter.WriteObject(keyEntry.Key); - pemWriter.Writer.Flush(); + // Convert the private key to unencrypted PEM format + using var stringWriter = new StringWriter(); + var pemWriter = new PemWriter(stringWriter); + pemWriter.WriteObject(keyEntry.Key); + pemWriter.Writer.Flush(); - return stringWriter.ToString(); - } - } - } + Logger.LogDebug("Private key found for alias {Alias}, returning private key", alias); + return stringWriter.ToString(); } + Logger.LogDebug("Alias '{Alias}' not found, returning null private key", alias); return null; // Private key with the given alias not found - - } protected string getK8SStorePassword(V1Secret certData) { Logger.LogDebug("Entered getK8SStorePassword()"); - Logger.LogDebug("Attempting to get store password from K8S secret."); - var storePasswordBytes = new byte[] { }; + Logger.LogDebug("Attempting to get store password from K8S secret"); + var storePasswordBytes = Array.Empty(); // if secret is a buddy pass if (!string.IsNullOrEmpty(StorePassword)) { - Logger.LogDebug("StorePassword is not null or empty, using StorePassword"); - var passwordHash = GetSHA256Hash(StorePassword); - Logger.LogTrace("Password hash: " + passwordHash); + Logger.LogDebug("Using provided 'StorePassword'"); + // var passwordHash = GetSHA256Hash(StorePassword); + // Logger.LogTrace("Password hash: " + passwordHash); storePasswordBytes = Encoding.UTF8.GetBytes(StorePassword); } else if (!string.IsNullOrEmpty(StorePasswordPath)) { // Split password path into namespace and secret name - Logger.LogDebug("Store password is null or empty, using StorePasswordPath"); + Logger.LogDebug( + "Store password is null or empty and StorePasswordPath is set, attempting to read password from K8S buddy secret"); Logger.LogTrace("Password path: {Path}", StorePasswordPath); - Logger.LogDebug("Splitting password path by /"); + Logger.LogTrace("Splitting password path by /"); var passwordPath = StorePasswordPath.Split("/"); Logger.LogDebug("Password path length: {Len}", passwordPath.Length.ToString()); var passwordNamespace = ""; var passwordSecretName = ""; if (passwordPath.Length == 1) { - Logger.LogDebug("Password path length is 1, using KubeNamespace."); + Logger.LogDebug("Password path length is 1, using KubeNamespace"); passwordNamespace = KubeNamespace; Logger.LogTrace("Password namespace: {Namespace}", passwordNamespace); passwordSecretName = passwordPath[0]; @@ -1034,17 +1208,30 @@ protected string getK8SStorePassword(V1Secret certData) Logger.LogDebug("Attempting to read K8S buddy secret"); var k8sPasswordObj = KubeClient.ReadBuddyPass(passwordSecretName, passwordNamespace); storePasswordBytes = k8sPasswordObj.Data[PasswordFieldName]; - var passwordHash = GetSHA256Hash(Encoding.UTF8.GetString(storePasswordBytes)); - Logger.LogTrace("Password hash: {Pwd}", passwordHash); + // var passwordHash = GetSHA256Hash(Encoding.UTF8.GetString(storePasswordBytes)); + // Logger.LogTrace("Password hash: {Pwd}", passwordHash); + if (storePasswordBytes == null) + { + Logger.LogError("Password not found in K8S buddy secret"); + throw new InvalidK8SSecretException( + "Password not found in K8S buddy secret"); // todo: should this be thrown? + } + Logger.LogDebug("K8S buddy secret read successfully"); } else if (certData != null && certData.Data.TryGetValue(PasswordFieldName, out var value1)) { Logger.LogDebug("Attempting to read password from PasswordFieldName"); storePasswordBytes = value1; - var passwordHash = GetSHA256Hash(Encoding.UTF8.GetString(storePasswordBytes)); - Logger.LogTrace("Password hash: {Pwd}", passwordHash); - Logger.LogDebug("Password read successfully."); + // var passwordHash = GetSHA256Hash(Encoding.UTF8.GetString(storePasswordBytes)); + // Logger.LogTrace("Password hash: {Pwd}", passwordHash); + if (storePasswordBytes == null) + { + Logger.LogError("Password not found in K8S secret"); + throw new InvalidK8SSecretException("Password not found in K8S secret"); // todo: should this be thrown? + } + + Logger.LogDebug("Password read successfully"); } else { @@ -1052,13 +1239,15 @@ protected string getK8SStorePassword(V1Secret certData) var passwdEx = ""; if (!string.IsNullOrEmpty(StorePasswordPath)) { - passwdEx = "Store secret '" + StorePasswordPath + "'did not contain key '" + CertificateDataFieldName + "' or '" + PasswordFieldName + "'." + - " Please provide a valid store password and try again."; + passwdEx = "Store secret '" + StorePasswordPath + "'did not contain key '" + CertificateDataFieldName + + "' or '" + PasswordFieldName + "'" + + " Please provide a valid store password and try again"; } else { - passwdEx = "Invalid store password. Please provide a valid store password and try again."; + passwdEx = "Invalid store password. Please provide a valid store password and try again"; } + Logger.LogError("{Msg}", passwdEx); throw new Exception(passwdEx); } @@ -1066,63 +1255,89 @@ protected string getK8SStorePassword(V1Secret certData) //convert password to string var storePassword = Encoding.UTF8.GetString(storePasswordBytes); // Logger.LogTrace("Store password: {Pwd}", storePassword); - var passwordHash2 = GetSHA256Hash(storePassword); - Logger.LogTrace("Password hash: {Pwd}", passwordHash2); + // var passwordHash2 = GetSHA256Hash(storePassword); + // Logger.LogTrace("Password hash: {Pwd}", passwordHash2); + Logger.LogDebug("Returning store password"); return storePassword; } protected Pkcs12Store LoadPkcs12Store(byte[] pkcs12Data, string password) { + Logger.LogDebug("Entered LoadPkcs12Store()"); var storeBuilder = new Pkcs12StoreBuilder(); var store = storeBuilder.Build(); + Logger.LogDebug("Attempting to load PKCS12 store"); using var pkcs12Stream = new MemoryStream(pkcs12Data); if (password != null) store.Load(pkcs12Stream, password.ToCharArray()); + Logger.LogDebug("PKCS12 store loaded successfully"); return store; } - protected string getCertificatePem(Pkcs12Store store, string password, string alias = "") + protected string GetCertificatePem(Pkcs12Store store, string password, string alias = "") { + Logger.LogDebug("Entered GetCertificatePem()"); if (string.IsNullOrEmpty(alias)) { alias = store.Aliases.Cast().FirstOrDefault(store.IsKeyEntry); } + + Logger.LogDebug("Attempting to get certificate with alias {Alias}", alias); var cert = store.GetCertificate(alias).Certificate; using var stringWriter = new StringWriter(); var pemWriter = new PemWriter(stringWriter); + + Logger.LogDebug("Attempting to write certificate to PEM format"); pemWriter.WriteObject(cert); pemWriter.Writer.Flush(); + Logger.LogTrace("certificate:\n{Cert}", stringWriter.ToString()); + + Logger.LogDebug("Returning certificate in PEM format"); return stringWriter.ToString(); } + protected string getPrivateKeyPem(Pkcs12Store store, string password, string alias = "") { + Logger.LogDebug("Entered getPrivateKeyPem()"); if (string.IsNullOrEmpty(alias)) { - alias = store.Aliases.Cast().FirstOrDefault(store.IsKeyEntry); + Logger.LogDebug("Alias is empty, attempting to get key entry alias"); + alias = store.Aliases.FirstOrDefault(store.IsKeyEntry); } + + Logger.LogDebug("Attempting to get private key with alias {Alias}", alias); var privateKey = store.GetKey(alias).Key; using var stringWriter = new StringWriter(); var pemWriter = new PemWriter(stringWriter); + + Logger.LogDebug("Attempting to write private key to PEM format"); pemWriter.WriteObject(privateKey); pemWriter.Writer.Flush(); + // Logger.LogTrace("private key:\n{Key}", stringWriter.ToString()); + Logger.LogDebug("Returning private key in PEM format for alias '{Alias}'", alias); return stringWriter.ToString(); } protected List getCertChain(Pkcs12Store store, string password, string alias = "") { + Logger.LogDebug("Entered getCertChain()"); if (string.IsNullOrEmpty(alias)) { + Logger.LogDebug("Alias is empty, attempting to get key entry alias"); alias = store.Aliases.Cast().FirstOrDefault(store.IsKeyEntry); } + var chain = new List(); + Logger.LogDebug("Attempting to get certificate chain with alias {Alias}", alias); var chainCerts = store.GetCertificateChain(alias); foreach (var chainCert in chainCerts) { + Logger.LogTrace("Adding certificate to chain"); using var stringWriter = new StringWriter(); var pemWriter = new PemWriter(stringWriter); pemWriter.WriteObject(chainCert.Certificate); @@ -1130,6 +1345,8 @@ protected List getCertChain(Pkcs12Store store, string password, string a chain.Add(stringWriter.ToString()); } + Logger.LogTrace("Certificate chain:\n{Chain}", string.Join("\n", chain)); + Logger.LogDebug("Returning certificate chain"); return chain; } @@ -1149,16 +1366,14 @@ public static bool IsDerFormat(byte[] data) public static string ConvertDerToPem(byte[] data) { var pemObject = new PemObject("CERTIFICATE", data); - using (var stringWriter = new StringWriter()) - { - var pemWriter = new PemWriter(stringWriter); - pemWriter.WriteObject(pemObject); - pemWriter.Writer.Flush(); - return stringWriter.ToString(); - } + using var stringWriter = new StringWriter(); + var pemWriter = new PemWriter(stringWriter); + pemWriter.WriteObject(pemObject); + pemWriter.Writer.Flush(); + return stringWriter.ToString(); } - public string GetSHA256Hash(string input) + protected static string GetSHA256Hash(string input) { var passwordHashBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(input)); var passwordHash = BitConverter.ToString(passwordHashBytes).Replace("-", "").ToLower(); @@ -1215,4 +1430,4 @@ public JkSisPkcs12Exception(string message, Exception innerException) : base(message, innerException) { } -} +} \ No newline at end of file diff --git a/kubernetes-orchestrator-extension/Jobs/Management.cs b/kubernetes-orchestrator-extension/Jobs/Management.cs index 3906057..2131c79 100644 --- a/kubernetes-orchestrator-extension/Jobs/Management.cs +++ b/kubernetes-orchestrator-extension/Jobs/Management.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, @@ -26,7 +26,7 @@ public class Management : JobBase, IManagementJobExtension { public Management(IPAMSecretResolver resolver) { - Resolver = resolver; + _resolver = resolver; } //Job Entry Point @@ -578,6 +578,16 @@ private JobResult HandleCreateOrUpdate(string secretType, ManagementJobConfigura // Split alias by / and get second to last element KubeSecretType //pattern: namespace/secrets/secret_type/secert_name var clusterSplitAlias = jobCertObj.Alias.Split("/"); + + // Check splitAlias length + if (clusterSplitAlias.Length < 3) + { + var invalidAliasErrMsg = "Invalid alias format for K8SCluster store type. Alias"; + Logger.LogError(invalidAliasErrMsg); + Logger.LogInformation("End MANAGEMENT job " + config.JobId + " " + invalidAliasErrMsg + " Failed!"); + return FailJob(invalidAliasErrMsg, config.JobHistoryId); + } + KubeSecretType = clusterSplitAlias[^2]; KubeSecretName = clusterSplitAlias[^1]; KubeNamespace = clusterSplitAlias[0]; diff --git a/kubernetes-orchestrator-extension/Jobs/PAMUtilities.cs b/kubernetes-orchestrator-extension/Jobs/PAMUtilities.cs new file mode 100644 index 0000000..2749e54 --- /dev/null +++ b/kubernetes-orchestrator-extension/Jobs/PAMUtilities.cs @@ -0,0 +1,39 @@ +// Copyright 2024 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. + +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; +using Unity.Injection; + +namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs; + +class PAMUtilities +{ + internal static string ResolvePAMField(IPAMSecretResolver resolver, ILogger logger, string name, string key) + { + logger.LogDebug("Attempting to resolve PAM eligible field '{Name}'", name); + if (string.IsNullOrEmpty(key)) + { + logger.LogWarning("PAM field is empty, skipping PAM resolution"); + return key; + } + + // test if field is JSON string + if (key.StartsWith("{") && key.EndsWith("}")) + { + var resolved = resolver.Resolve(key); + if (string.IsNullOrEmpty(resolved)) + { + logger.LogWarning("Failed to resolve PAM field {Name}", name); + } + return resolved; + } + + logger.LogDebug("Field '{Name}' is not a JSON string, skipping PAM resolution", name); + return key; + } +} diff --git a/kubernetes-orchestrator-extension/Jobs/Reenrollment.cs b/kubernetes-orchestrator-extension/Jobs/Reenrollment.cs index 0bcc32b..84f738d 100644 --- a/kubernetes-orchestrator-extension/Jobs/Reenrollment.cs +++ b/kubernetes-orchestrator-extension/Jobs/Reenrollment.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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 @@ -25,7 +25,7 @@ public class Reenrollment : JobBase, IReenrollmentJobExtension { public Reenrollment(IPAMSecretResolver resolver) { - Resolver = resolver; + _resolver = resolver; } //Job Entry Point public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment) diff --git a/kubernetes-orchestrator-extension/Keyfactor.Orchestrators.K8S.csproj b/kubernetes-orchestrator-extension/Keyfactor.Orchestrators.K8S.csproj index 5f3befe..ce85ab7 100644 --- a/kubernetes-orchestrator-extension/Keyfactor.Orchestrators.K8S.csproj +++ b/kubernetes-orchestrator-extension/Keyfactor.Orchestrators.K8S.csproj @@ -21,10 +21,10 @@ - - + + - + diff --git a/kubernetes-orchestrator-extension/Models/SerializedStoreInfo.cs b/kubernetes-orchestrator-extension/Models/SerializedStoreInfo.cs index f9e3cf9..c736d2c 100644 --- a/kubernetes-orchestrator-extension/Models/SerializedStoreInfo.cs +++ b/kubernetes-orchestrator-extension/Models/SerializedStoreInfo.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, diff --git a/kubernetes-orchestrator-extension/PamUtilities.cs b/kubernetes-orchestrator-extension/PamUtilities.cs deleted file mode 100644 index 7fe6567..0000000 --- a/kubernetes-orchestrator-extension/PamUtilities.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 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. - -using Keyfactor.Orchestrators.Extensions.Interfaces; -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.Orchestrator.K8S; - -class PamUtilities -{ - internal static string ResolvePamField(IPAMSecretResolver resolver, ILogger logger, string name, string key) - { - logger.LogDebug($"Attempting to resolve PAM eligible field {name} with key {key}"); - return resolver.Resolve(key); - } -} diff --git a/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs b/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs index a9db31a..c57fb80 100644 --- a/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs +++ b/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, diff --git a/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs b/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs index c1257fb..96c4746 100644 --- a/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs +++ b/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Keyfactor +// Copyright 2024 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, diff --git a/readme_source.md b/readme_source.md index 2b735f0..c6c7709 100644 --- a/readme_source.md +++ b/readme_source.md @@ -370,9 +370,9 @@ kfutil store-types create --name K8SSecret ##### UI Custom Fields Tab | Name | Display Name | Type | Required | Default Value | Description | |------------------|---------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | `default` | The K8S namespace the `Opaque` secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | ✓ | | The name of the K8S `Opaque` secret. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `secret` | | +| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `Opaque` secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `Opaque` secret. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `secret` | This is required and must be `secret`. | | IncludeCertChain | Include Certificate Chain | Bool | | `true` | Will default to `true` if not set. If set to `false` only leaf cert will be deployed. | | SeparateChain | SeparateChain | Bool | | `false` | Will default to `false` if not set. `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | @@ -420,13 +420,13 @@ kfutil store-types create --name K8STLSSecr ![k8sstlssecr_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8sstlssecr_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|------------------|----------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `tls` secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `tls` secret. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `tls_secret` | | -| IncludeCertChain | Include Certificate Chain | Bool | | `true` | If set to `false` only leaf cert will be deployed. | -| SeparateChain | SeparateChain | Bool | | `true` | `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | +| Name | Display Name | Type | Required | Default Value | Description | +|------------------|----------------------------|--------|----------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | The K8S namespace the `tls` secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S `tls` secret. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `tls_secret` | This is required and must be `tls_secret`. | +| IncludeCertChain | Include Certificate Chain | Bool | | `true` | If set to `false` only leaf cert will be deployed. | +| SeparateChain | SeparateChain | Bool | | `true` | `true` will deploy leaf cert to `tls.crt` and the rest of the cert chain to `ca.crt`. If set to `false` the full chain is deployed to `tls.crt` | ![k8sstlssecr_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8sstlssecr_custom_fields.png) @@ -473,16 +473,16 @@ kfutil store-types create --name K8SPKCS12 ![k8spkcs12_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8spkcs12_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | K8S namespace the PKCS12 secret lives. This will override any value inferred in the `Store Path` | -| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains PKCS12 data. This will override any value inferred in the `Store Path` | -| KubeSecretType | Kube Secret Type | String | ✓ | `pkcs12` | This must be set to `pkcs12`. | -| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.p12` | The K8S secret field name to source the PKCS12 data from. You can provide an extension `.p12` or `.pfx` for a secret with a key `example.p12` | -| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the PKCS12 password from a K8S secret this is the field it will look for the password in. | -| PasswordIsK8SSecret | Password Is K8S Secret | Bool | ✓ | `false` | If you want to use the PKCS12 secret or a separate secret specific in `KubeSecretPasswordPath` set this to `true` | -| StorePassword | Kube Secret Password | Secret | | | If you want to specify the PKCS12 password on the store in Command use this. | -| StorePasswordPath | Kube Secret Password Path | String | | | Source PKCS12 password from a separate K8S secret. Pattern: `namespace_name/secret_name` | +| Name | Display Name | Type | Required | Default Value | Description | +|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | K8S namespace the PKCS12 secret lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains PKCS12 data. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `pkcs12` | This is required and must be set to `pkcs12`. | +| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.p12` | The K8S secret field name to source the PKCS12 data from. You can provide an extension `.p12` or `.pfx` for a secret with a key `example.p12` | +| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the PKCS12 password from the PKCS12 K8S secret, or a separate K8S secret, this is the field it will look for the password in. | +| PasswordIsK8SSecret | Password Is K8S Secret | Bool | | `false` | If sourcing the PKCS12 password from the PKCS12 K8S secret, or a separate K8S secret specified in `StorePasswordPath` set this to `true` | +| StorePassword | Store Password | Secret | | | If sourcing the PKCS12 password from Command use this. *Note* this will take precedence over `PasswordIsK8SSecret` | +| StorePasswordPath | Kube Secret Password Path | String | | | If sourcing the PKCS12 password from a separate K8S secret. Pattern: `namespace_name/secret_name` | ![k8spkcs12_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8spkcs12_custom_fields.png) @@ -537,16 +537,16 @@ kfutil store-types create --name K8SJKS ![k8sjks_advanced.png](docs%2Fscreenshots%2Fstore_types%2Fk8sjks_advanced.png) ##### UI Custom Fields Tab -| Name | Display Name | Type | Required | Default Value | Description | -|--------------------------|-----------------------------|--------|----------|---------------|--------------------------------------------------------------------------------------------------------| -| KubeNamespace | Kube Namespace | String | | | K8S namespace the JKS secret lives. This will override any value inferred in the `Store Path`. | -| KubeSecretName | Kube Secret Name | String | | | The K8S secret name that contains JKS data. This will override any value inferred in the `Store Path`. | -| KubeSecretType | Kube Secret Type | String | ✓ | `jks` | | -| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.jks` | The K8S secret field name to source the JKS data from | -| PasswordFieldName | Password Field Name | String | ✓ | `password` | If sourcing the JKS password from a K8S secret this is the field it will look for the password in. | -| PasswordIsK8SSecret | Password Is K8S Secret | Bool | ✓ | `false` | If you want to use the JKS secret or a separate secret specific in `` set this to `true` | -| StorePassword | Kube Secret Password | Secret | | | If you want to specify the JKS password on the store in Command use this. | -| StorePasswordPath | Kube Secret Password Path | String | | | Source JKS password from a separate K8S secret. Pattern: `namespace_name/secret_name` | +| Name | Display Name | Type | Required | Default Value | Description | +|--------------------------|-----------------------------|--------|----------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| KubeNamespace | Kube Namespace | String | | | K8S namespace the K8S secret containing the JKS data lives. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeSecretName`. | +| KubeSecretName | Kube Secret Name | String | | | The name of the K8S secret that contains the JKS data. This will override any value inferred in the `Store Path`. To be used in conjunction with `KubeNamespace`. | +| KubeSecretType | Kube Secret Type | String | ✓ | `jks` | This is required and must be `jks`. | +| CertificateDataFieldName | Certificate Data Field Name | String | ✓ | `.jks` | The K8S secret field name to source the JKS data from. | +| PasswordFieldName | Password Field Name | String | | `password` | If sourcing the JKS password from a another field on the K8S JKS secret or the separate K8S secret, this is the secret field name it will look for the password in. | +| PasswordIsK8SSecret | Password Is K8S Secret | Bool | | `false` | If sourcing the JKS password from the K8S JKS secret or a separate K8S secret set this to `true` | +| StorePassword | Store Password | Secret | | | If sourcing the JKS password from Command use this. *Note* this will take precedence over `PasswordIsK8SSecret` | +| StorePasswordPath | Kube Secret Password Path | String | | | Source the JKS password from a separate K8S secret. Pattern: `namespace_name/secret_name`. *Note* the Orchestrator K8S service account must have read access to the secret. | ![k8sjks_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8sjks_custom_fields.png) @@ -709,7 +709,7 @@ kfutil store-types create --name K8SCert |--------------------|---------------------------|--------|----------|---------------|--------------------------------------------------------------------------------------------------------| | KubeNamespace | Kube Namespace | String | | | The K8S namespace the `cert` resource lives. This will override any value inferred in the `Store Path` | | KubeSecretName | Kube Secret Name | String | | | The K8S `cert` name. This will override any value inferred in the `Store Path`. | -| KubeSecretType | Kube Secret Type | String | ✓ | `cert` | | +| KubeSecretType | Kube Secret Type | String | ✓ | `cert` | This is required and must be `cert`. | ![k8scert_custom_fields.png](docs%2Fscreenshots%2Fstore_types%2Fk8scert_custom_fields.png) diff --git a/scripts/kubernetes/create_service_account.sh b/scripts/kubernetes/create_service_account.sh old mode 100644 new mode 100755 index fb49fbc..8792e15 --- a/scripts/kubernetes/create_service_account.sh +++ b/scripts/kubernetes/create_service_account.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash +set -e + unset KUBECONFIG -kubectl apply -f ./kubernetes_svc_account.yml +kubectl apply -f ./kubernetes_svc_account.yaml # Define the name of the Kubernetes namespace where the service account resides NAMESPACE="${K8S_NAMESPACE:-default}" @@ -20,8 +22,10 @@ echo "CLUSTER_API_SERVER: $CLUSTER_API_SERVER" # Get the service account token -SA_TOKEN=$(kubectl get secrets -n $NAMESPACE | grep -i $SA_NAME | awk '{print $1}') -SA_TOKEN=$(kubectl get secret/$SA_TOKEN -n $NAMESPACE -o json | jq -r '.data.token' | base64 --decode) +SA_TOKEN=$(kubectl get secrets -n "$NAMESPACE" | grep -i "$SA_NAME" | awk '{print $1}') +#echo "SA_TOKEN: $SA_TOKEN" #uncomment if you need to debug +SA_TOKEN=$(kubectl get "secret/$SA_TOKEN" -n "$NAMESPACE" -o json | jq -r '.data.token' | base64 --decode) +#echo "SA_TOKEN: $SA_TOKEN" ### NOTE - If you have more than one cluster, you may need to change the index of the array ### CA_CERT=$(kubectl config view --raw -o json | jq -r '.clusters[0].cluster."certificate-authority-data"') @@ -57,6 +61,7 @@ users: # Save only the service account context to a JSON file kubectl config view --raw -o json --kubeconfig=kubeconfig > "${SA_NAME}-context.json" +echo "${SA_NAME}-context.json has been created. Please copy and paste the content as the 'ServerPassword' value on your certificate store definition." # Set the KUBECONFIG environment variable to point to the new kubeconfig file export KUBECONFIG=$PWD/kubeconfig diff --git a/scripts/kubernetes/get_service_account_creds.sh b/scripts/kubernetes/get_service_account_creds.sh index a2ce685..1f6b839 100644 --- a/scripts/kubernetes/get_service_account_creds.sh +++ b/scripts/kubernetes/get_service_account_creds.sh @@ -35,4 +35,4 @@ users: token: $SA_TOKEN" > kubeconfig kubectl config view --raw -o json --kubeconfig=kubeconfig > "${SA_NAME}-context.json" -echo "${SA_NAME}-context.json has been created. Please copy and paste the content as the 'KubeSvcCreds' value on your certificate store definition." \ No newline at end of file +echo "${SA_NAME}-context.json has been created. Please copy and paste the content as the 'ServerPassword' value on your certificate store definition." \ No newline at end of file diff --git a/scripts/kubernetes/kubernetes_svc_account.yaml b/scripts/kubernetes/kubernetes_svc_account.yaml index 861af9a..4baaeac 100644 --- a/scripts/kubernetes/kubernetes_svc_account.yaml +++ b/scripts/kubernetes/kubernetes_svc_account.yaml @@ -3,15 +3,13 @@ kind: ConfigMap metadata: name: keyfactor-orchestrator-config data: - namespace: default - service_account: keyfactor-orchestrator-sa + namespace: "default" + service_account: "keyfactor-orchestrator-sa" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole - metadata: name: keyfactor-orchestrator - rules: - apiGroups: [""] resources: ["secrets"] @@ -30,21 +28,17 @@ rules: --- apiVersion: v1 kind: ServiceAccount - metadata: name: keyfactor-orchestrator-sa - --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: keyfactor-orchestrator-sa-binding - roleRef: kind: ClusterRole name: keyfactor-orchestrator apiGroup: rbac.authorization.k8s.io - subjects: - kind: ServiceAccount name: keyfactor-orchestrator-sa @@ -53,7 +47,7 @@ subjects: apiVersion: v1 kind: Secret metadata: - name: keyfactor + name: keyfactor-orchestrator-sa annotations: kubernetes.io/service-account.name: keyfactor-orchestrator-sa type: kubernetes.io/service-account-token \ No newline at end of file diff --git a/update_store_types.sh b/update_store_types.sh new file mode 100755 index 0000000..b03661a --- /dev/null +++ b/update_store_types.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +function updateFromCommandInstance() { + kfutil store-types get --name K8SCLUSTER --output-to-integration-manifest + kfutil store-types get --name K8SNS --output-to-integration-manifest + kfutil store-types get --name K8SJKS --output-to-integration-manifest + kfutil store-types get --name K8SPKCS12 --output-to-integration-manifest + kfutil store-types get --name K8STLSSecr --output-to-integration-manifest + kfutil store-types get --name K8SSecret --output-to-integration-manifest + kfutil store-types get --name K8SCert --output-to-integration-manifest +} + +function integrationManifestToFiles(){ + store_types_length=$(jq '.about.orchestrator.store_types | length' integration-manifest.json) + + for (( i=0; i<$store_types_length; i++ )) + do + short_name=$(jq -r ".about.orchestrator.store_types[$i].ShortName" integration-manifest.json) + jq ".about.orchestrator.store_types[$i]" integration-manifest.json > "$short_name.json" + done +} + +integrationManifestToFiles \ No newline at end of file