├── .github └── CODEOWNERS ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PLUGIN-HASHES.md ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── plugin └── pki │ ├── backend.go │ ├── backend_test.go │ ├── ca_test.go │ ├── ca_util.go │ ├── cert_util.go │ ├── cert_util_test.go │ ├── crl_test.go │ ├── crl_util.go │ ├── fields.go │ ├── path_config_ca.go │ ├── path_config_crl.go │ ├── path_config_urls.go │ ├── path_fetch.go │ ├── path_import_queue.go │ ├── path_import_queue_test.go │ ├── path_intermediate.go │ ├── path_issue_sign.go │ ├── path_revoke.go │ ├── path_roles.go │ ├── path_roles_test.go │ ├── path_root.go │ ├── path_tidy.go │ ├── path_venafi_policy.go │ ├── path_venafi_policy_sync.go │ ├── path_venafi_policy_sync_test.go │ ├── path_venafi_policy_test.go │ ├── path_venafi_secret.go │ ├── path_venafi_secret_test.go │ ├── scheduler.go │ ├── scheduler_test.go │ ├── secret_certs.go │ ├── util.go │ ├── util_test.go │ ├── vcert.go │ ├── venafi_integration_test.go │ └── venafi_util.go ├── scripts ├── allowed_csr.conf ├── allowed_empty_csr.conf ├── build.sh ├── gen_test_csr.sh ├── gofmtcheck.sh ├── vault-config-with-consul.hcl ├── wait-for-it.sh └── wrong_csr.conf └── vault-config.hcl /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | *.go @rvelaVenafi @marcos-albornoz @luispresuelVenafi @EduardoVV 2 | *.md @jdw2465VEN @tr1ck3r 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | pkg 3 | pki_custom 4 | credentials 5 | demo.sh 6 | !*/** 7 | *.DS_Store 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | linters: 5 | disable: 6 | - deadcode 7 | - varcheck 8 | - unused 9 | enable: 10 | - gosec 11 | 12 | issues: 13 | exclude-rules: 14 | - text: "G505" 15 | linters: 16 | - gosec 17 | - text: "G401" 18 | linters: 19 | - gosec -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.9.0 (February 10, 2021) 2 | 3 | Updated Venafi Cloud integration to use OutagePREDICT instead of DevOpsACCELERATE. 4 | 5 | ## v0.8.3 (January 23, 2021) 6 | 7 | Enhanced monitoring behavior to allow limiting Venafi import to certificates a Vault CA issues that do not comply with Venafi policy. 8 | 9 | ## v0.8.2 (December 11, 2020) 10 | 11 | Updated credential requirements for Trust Protection Platform to support initialization with only a `refresh_token`. 12 | 13 | ## v0.8.1 (October 30, 2020) 14 | 15 | Introduced Venafi Secret for specifying Venafi connection and authentication settings. 16 | 17 | Added support for token authentication with Trust Protection Platform (API Application ID "hashicorp-vault-monitor-by-venafi"). 18 | 19 | Deprecated legacy username/password for Trust Protection Platform. 20 | 21 | Resolved Vault Enterprise issue involving behavior when interacting with Performance Standby or Performance Secondary. 22 | 23 | Added option to automatically synchronize PKI role settings with Venafi Policy. 24 | 25 | Updated Venafi Policy to solely govern the roles to which it enforces policy and default values, and roles from which it imports certificates into Venafi. 26 | 27 | Dropped support for `apikey`, `tpp_url`, `tpp_username`, `tpp_password`, `zone`, `trust_bundle_file`, `venafi_import`, `venafi_import_timeout`, `venafi_import_workers`, and `venafi_check_policy` role settings. 28 | 29 | Added Source Application Tagging for Trust Protection Platform and Venafi Cloud. 30 | 31 | ## v0.6.0 (February 19, 2020) 32 | 33 | Dropped support for previously deprecated `tpp_import`, `tpp_import_timeout`, and `tpp_import_workers` parameters. 34 | 35 | ## v0.5.5 (February 5, 2020) 36 | 37 | Resolved issue where Vault stopped issuing certifcates after importing hundreds/thousands of certificates into Venafi. 38 | 39 | ## v0.5.3 (January 20, 2020) 40 | 41 | Resolved issue where secrets engine would try indefinitely to import certificates that were rejected because they don't comply with policy (i.e. key reused) 42 | 43 | ## v0.5.2 (January 10, 2020) 44 | 45 | Resolved issue involving Venafi Policy enforcement of key size 46 | 47 | ## v0.5.0+311 (September 13, 2019) 48 | 49 | Resolved issue involving Venafi Policy enforcement of domains with TPP. 50 | 51 | ## v0.4.0+181 (May 16, 2019) 52 | 53 | Resolved issue with plugin running Vault on Windows. 54 | 55 | ## v0.4.0 (April 25, 2019) 56 | 57 | Added visibility into certificates issued by the Vault CA for Venafi Cloud. 58 | 59 | ## v0.3.2 (April 11, 2019) 60 | 61 | Enhanced secrets engine to start import queue automatically after Vault restart. 62 | 63 | Offer "strict" and "optional" plugin binaries to choose whether compliance with Venafi Policy is required ("optional" targeting test/dev use cases). 64 | 65 | ## v0.3.0 (March 16, 2019) 66 | 67 | Added Venafi Policy Enforcement to check certificate requests for compliance with Venafi Policy. 68 | 69 | ## v0.1.0 (February 6, 2019) 70 | 71 | Initial Release, provides visibility into certificates issued by the Vault CA for Trust Protection Platform. 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM vault:1.0.2 2 | 3 | # /vault/logs is made available to use as a location to store audit logs, if 4 | # desired; /vault/file is made available to use as a location with the file 5 | # storage backend, if desired; the server will be started with /vault/config as 6 | # the configuration directory so you can add additional config files in that 7 | # location. 8 | RUN mkdir -p /tools && \ 9 | mkdir -p /vault/logs /vault/file /vault/config && \ 10 | chown -R vault:vault /vault 11 | 12 | # Expose the logs directory as a volume since there's potentially long-running 13 | # state in there 14 | VOLUME /vault/logs 15 | 16 | # Expose the file directory as a volume since there's potentially long-running 17 | # state in there 18 | VOLUME /vault/file 19 | 20 | ADD pkg/bin/vault-pki-monitor-venafi /vault_plugin/vault-pki-monitor-venafi 21 | 22 | #Add helper scripts 23 | ADD scripts /scripts 24 | 25 | #Add consul configs 26 | ADD scripts/vault-config-with-consul.hcl /config/ 27 | 28 | 29 | # 8200/tcp is the primary interface that applications use to interact with 30 | # Vault. 31 | EXPOSE 8200 32 | 33 | # By default you'll get a single-node development server that stores everything 34 | # in RAM and bootstraps itself. Don't use this configuration for production. 35 | CMD ["server", "-dev"] 36 | 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Metadata about this makefile and position 2 | MKFILE_PATH := $(lastword $(MAKEFILE_LIST)) 3 | CURRENT_DIR := $(patsubst %/,%,$(dir $(realpath $(MKFILE_PATH)))) 4 | 5 | # List of tests to run 6 | TEST ?= $$(go list ./... | grep -v /vendor/ | grep -v /e2e) 7 | TEST_TIMEOUT?=20m 8 | GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) 9 | 10 | #Plugin information 11 | PLUGIN_NAME := venafi-pki-monitor 12 | PLUGIN_DIR := pkg/bin 13 | PLUGIN_PATH := $(PLUGIN_DIR)/$(PLUGIN_NAME) 14 | DIST_DIR := pkg/dist 15 | VERSION=`git describe --abbrev=0 --tags` 16 | 17 | ifdef BUILD_NUMBER 18 | VERSION:=$(VERSION)+$(BUILD_NUMBER) 19 | endif 20 | 21 | ifdef RELEASE_VERSION 22 | ifneq ($(RELEASE_VERSION),none) 23 | VERSION=$(RELEASE_VERSION) 24 | endif 25 | endif 26 | 27 | #test demo vars 28 | IMPORT_DOMAIN := import.example.com 29 | IMPORT_ROLE := import 30 | MOUNT := pki 31 | RANDOM_SITE_EXP := $$(head /dev/urandom | docker run --rm -i busybox tr -dc a-z0-9 | head -c 5 ; echo '') 32 | ROLE_OPTIONS := generate_lease=true ttl=1h max_ttl=1h 33 | SHA256 := $$(shasum -a 256 "$(PLUGIN_PATH)" | cut -d' ' -f1) 34 | TRUST_BUNDLE := /opt/venafi/bundle.pem 35 | 36 | #Docker vars 37 | VAULT_CONT := $$(docker-compose ps |grep Up|grep vault_1|awk '{print $$1}') 38 | DOCKER_CMD := docker exec -it $(VAULT_CONT) 39 | VAULT_CMD := $(DOCKER_CMD) vault 40 | SHA256_DOCKER_CMD := sha256sum "/vault_plugin/$(PLUGIN_NAME)" | cut -d' ' -f1 41 | 42 | ### Exporting variables for demo and tests 43 | .EXPORT_ALL_VARIABLES: 44 | VAULT_ADDR = http://127.0.0.1:8200 45 | #Must be set,otherwise cloud certificates will timeout 46 | VAULT_CLIENT_TIMEOUT = 180s 47 | 48 | test: linter 49 | VAULT_ACC=1 \ 50 | go get gotest.tools/gotestsum 51 | gotestsum --junitfile junit.xml -- -timeout $(TEST_TIMEOUT) ./... 52 | 53 | policy_test: 54 | go test github.com/Venafi/vault-pki-monitor-venafi/plugin/pki -run ^TestBackend_VenafiPolicy*$ 55 | 56 | fmt: 57 | gofmt -w $(GOFMT_FILES) 58 | 59 | fmtcheck: 60 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" 61 | 62 | #Need to unset VAULT_TOKEN when running vault with dev parameter. 63 | unset: 64 | unset VAULT_TOKEN 65 | 66 | #Developement server tasks 67 | dev_server: unset 68 | pkill vault || echo "Vault server is not running" 69 | vault server -log-level=debug -dev -config=vault-config.hcl 70 | 71 | dev: dev_build mount_dev 72 | 73 | import: ca import_config_write import_config_read import_cert_write 74 | 75 | ca: 76 | vault write $(MOUNT)/root/generate/internal \ 77 | common_name=my-website.com \ 78 | ttl=8760h 79 | 80 | #Build 81 | clean: 82 | rm -rf $(PLUGIN_DIR) 83 | rm -rf $(DIST_DIR) 84 | rm -rf artifacts 85 | 86 | build: build_strict build_optional 87 | 88 | build_strict: 89 | scripts/build.sh $(PLUGIN_NAME) $(PLUGIN_DIR) $(DIST_DIR) strict $(VERSION) 90 | 91 | build_optional: 92 | scripts/build.sh $(PLUGIN_NAME) $(PLUGIN_DIR) $(DIST_DIR) optional $(VERSION) 93 | 94 | 95 | dev_build: 96 | sed -i 's/const venafiPolicyDenyAll =.*/const venafiPolicyDenyAll = true/' plugin/pki/vcert.go 97 | env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS_STRICT)' -a -o $(PLUGIN_DIR)/$(PLUGIN_NAME) || exit 1 98 | 99 | 100 | mount_dev: unset 101 | vault write sys/plugins/catalog/$(PLUGIN_NAME) sha_256="$(SHA256)" command="$(PLUGIN_NAME)" 102 | vault secrets disable $(MOUNT) || echo "Secrets already disabled" 103 | vault secrets enable -path=$(MOUNT) -plugin-name=$(PLUGIN_NAME) plugin 104 | 105 | import_config_write: 106 | vault write $(MOUNT)/roles/$(IMPORT_ROLE) \ 107 | venafi_import="true" \ 108 | tpp_url=$(TPP_URL) \ 109 | tpp_user=$(TPP_USER) \ 110 | tpp_password=$(TPP_PASSWORD) \ 111 | zone="$(TPP_ZONE)" \ 112 | $(ROLE_OPTIONS) \ 113 | allowed_domains=$(IMPORT_DOMAIN) \ 114 | allow_subdomains=true \ 115 | trust_bundle_file=$(TRUST_BUNDLE) \ 116 | import_timeout=15 \ 117 | import_workers=5 118 | 119 | import_config_read: 120 | vault read $(MOUNT)/roles/$(IMPORT_ROLE) 121 | 122 | import_cert_write: 123 | $(eval RANDOM_SITE := $(shell echo $(RANDOM_SITE_EXP))) 124 | @echo "Issuing import-$(RANDOM_SITE).$(IMPORT_DOMAIN)" 125 | vault write $(MOUNT)/issue/$(IMPORT_ROLE) common_name="import-$(RANDOM_SITE).$(IMPORT_DOMAIN)" alt_names="alt-$(RANDOM_SITE).$(IMPORT_DOMAIN),alt2-$(RANDOM_SITE).$(IMPORT_DOMAIN)" 126 | 127 | 128 | collect_artifacts: 129 | rm -rf artifacts 130 | mkdir -p artifacts 131 | cp -rv $(DIST_DIR)/*.zip artifacts 132 | 133 | release: 134 | echo '```' > release.txt 135 | cd artifacts; sha256sum * >> ../release.txt 136 | echo '```' >> release.txt 137 | go get -u github.com/tcnksm/ghr 138 | ghr -prerelease -n $$RELEASE_VERSION -body="$$(cat ./release.txt)" $$RELEASE_VERSION artifacts/ 139 | 140 | #Docker server with consul 141 | docker_server_prepare: 142 | @echo "Using vault client version $(VAULT_VERSION)" 143 | ifeq ($(VAULT_VERSION),v0.10.3) 144 | @echo "Vault version v0.10.3 have bug which prevents plugin to work properly. Please update your vault client" 145 | @exit 1 146 | endif 147 | 148 | docker_server_up: 149 | docker-compose up -d --build 150 | @echo "Run: docker-compose logs" 151 | @echo "to see the logs" 152 | @echo "Run: docker exec -it cault_vault_1 sh" 153 | @echo "to login into vault container" 154 | @echo "Waiting until server start" 155 | sleep 10 156 | 157 | 158 | docker_server_init: 159 | $(VAULT_CMD) operator init -key-shares=1 -key-threshold=1 160 | @echo "To unseal the vault run:" 161 | @echo "$(VAULT_CMD) operator unseal UNSEAL_KEY" 162 | 163 | docker_server_unseal: 164 | @echo Enter unseal key: 165 | $(VAULT_CMD) operator unseal 166 | 167 | docker_server_login: 168 | @echo Enter root token: 169 | $(VAULT_CMD) login 170 | 171 | docker_server_down: 172 | docker-compose down --remove-orphans 173 | 174 | docker_server_logs: 175 | docker-compose logs -f 176 | 177 | docker_server_sh: 178 | $(DOCKER_CMD) sh 179 | 180 | docker_server: docker_server_prepare docker_server_down docker_server_up docker_server_init docker_server_unseal docker_server_login mount_docker 181 | @echo "Vault started. To run make command export VAULT_TOKEN variable and run make with -e flag, for example:" 182 | @echo "export VAULT_TOKEN=enter-root-token-here" 183 | @echo "make cloud -e" 184 | 185 | mount_docker: 186 | $(eval SHA256 := $(shell echo $$($(DOCKER_CMD) $(SHA256_DOCKER_CMD)))) 187 | $(VAULT_CMD) write sys/plugins/catalog/$(PLUGIN_NAME) sha_256="$$SHA256" command="$(PLUGIN_NAME)" 188 | $(VAULT_CMD) secrets disable $(MOUNT) || echo "Secrets already disabled" 189 | $(VAULT_CMD) secrets enable -path=$(MOUNT) -plugin-name=$(PLUGIN_NAME) plugin 190 | 191 | linter: 192 | @golangci-lint --version || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /go/bin 193 | golangci-lint run --timeout 3m0s 194 | -------------------------------------------------------------------------------- /PLUGIN-HASHES.md: -------------------------------------------------------------------------------- 1 | # Secrets Engine Plugin Versions 2 | 3 | Unfortunately, the HashiCorp Vault plugin architecture does not provide developers with a way to 4 | communicate the actual version of their plugins to Vault administrators. Instead administrators 5 | must rely on the SHA256 hash of the plugin binary to differentiate one version of a plugin from 6 | another. 7 | 8 | Listed below are the SHA256 hashes of plugins from recent official releases, provided to help 9 | simplify the task of identifying which version of the *Venafi PKI Monitoring Secrets Engine for 10 | HashiCorp Vault* you are currently using. 11 | 12 | ### v0.9.0 13 | ``` 14 | 276a7122d16f6c2b4a27c4a9592e4d52a584cf6c92f899870b2d4053736cdeb1 darwin_optional venafi-pki-monitor_optional 15 | f59c3e7072c08c9cf2a5e19fb1dd93ffa0e9946ce950fb354ca2fb58fe12e235 darwin_strict venafi-pki-monitor 16 | 41b9642cb96090672c48f7964857286ea3edb830b6c2300bd26505d46971f885 linux86_optional venafi-pki-monitor_optional 17 | 9a53551910a29a0d457f1475730d271f4c7debe3e724c57886def86d3a3614b3 linux86_strict venafi-pki-monitor 18 | 5862ad98bc639a81faf55234f30b6aff6852c7a2fb289009bca7985db2122392 linux_optional venafi-pki-monitor_optional 19 | 977980444b0509e425877f484f234c71de4379781c7cdfc38bc487702a5e714d linux_strict venafi-pki-monitor 20 | 6d4bb8e8bb93e156e462986f177f81c9f4a80579b0051605d74228f7bf4ba567 windows86_optional venafi-pki-monitor_optional.exe 21 | 6f4d63bc59eb4f03db3d049043cb8ac66fda33c6577b420c0993ab426082493d windows86_strict venafi-pki-monitor.exe 22 | c0a869468a6d230979ce5023284c1bd423f1ba54b046f481191ed5db835b82ed windows_optional venafi-pki-monitor_optional.exe 23 | 69ca0415ea9e5b09060ce72a9f1e71e7c89213a60f3d3f1ec9625a9ed88b92d6 windows_strict venafi-pki-monitor.exe 24 | ``` 25 | 26 | ### v0.8.3 27 | ``` 28 | 43b5e3d3fd7eb3cc5f93e518a7bbe22eacda93b62eda1e45b7feb1d0c0c11188 darwin_optional venafi-pki-monitor_optional 29 | 8af40858fc39c1a21eeb5a26097a089e5a99d54052b7062a402bf3301600b2d7 darwin_strict venafi-pki-monitor 30 | 10aec6cb55947abd24c352eaa708c085daea38d8c38987aedac8cc892c9600ff linux86_optional venafi-pki-monitor_optional 31 | 10de301eb1e082c6ede21bed052177c98421f99bf9e1a196397cd3ac35aba636 linux86_strict venafi-pki-monitor 32 | 32af916f48676c4083549bd45ad59e0d0bac9fbf863caef2b6ad2bab3b92596c linux_optional venafi-pki-monitor_optional 33 | cb2186123fac03d6c9c8524505f46e383188a2605bb97e916d3c25aad42bfe93 linux_strict venafi-pki-monitor 34 | a1b293cf818ab1281447db0ae7e2cd77819ed908f03af5d393d5ae079aa5e706 windows86_optional venafi-pki-monitor_optional.exe 35 | c5a6432a5f222bc057a3023a93cd6ea99d05669db3f8dd9d41eabb9fd5303a1f windows86_strict venafi-pki-monitor.exe 36 | f2fb285904cd4f4c2813b2d5387d7525488abb8880e54f54e32d884136f922ff windows_optional venafi-pki-monitor_optional.exe 37 | a0e72036eb55193c2ee3c417c44fb5a4081556f3d67b3332ba6a772087de0569 windows_strict venafi-pki-monitor.exe 38 | ``` 39 | 40 | ### v0.8.2 41 | ``` 42 | 7ecac55684d69159829819c7bf764f837e65b34431fd46629fcb9de2ac989b51 darwin_optional venafi-pki-monitor_optional 43 | 0e68c90ce69f4d75d98994477eacd020945c5b832700e36bf954cb97ab0f9b31 darwin_strict venafi-pki-monitor 44 | 1ea31ffa88f88d5488a5ecba0b16dadec046b9872d69881edb483baea73db3c1 linux86_optional venafi-pki-monitor_optional 45 | 3cb74ef23d200108bdb09208ec8719128a83951165badbdc55a05f72c856e3ec linux86_strict venafi-pki-monitor 46 | 021842e629af5e41d6e1fee0d79b712efacb1e042525d4a4ebe092a40e5775ec linux_optional venafi-pki-monitor_optional 47 | 9fc2200565d24ff77e4a1679259461a2ad1a6ac8221254ab661b15cead026a7e linux_strict venafi-pki-monitor 48 | be4fbe88926ec0e6b97ed83071e97b0cadbbb4f3605d2e157f5738c3bbad4af7 windows86_optional venafi-pki-monitor_optional.exe 49 | 23a26e27090e53054f6bafd79693343a1e5f63b7ae88e0f748e377ecf88c3018 windows86_strict venafi-pki-monitor.exe 50 | cec348f58da70295b8df39b84769527ad02770de81ed1016da5dfe7ad21425c2 windows_optional venafi-pki-monitor_optional.exe 51 | 8f9c4d2a0b10bb477f9ec3ca052f7347d1caffb818024fefda06ee7042c860a9 windows_strict venafi-pki-monitor.exe 52 | ``` 53 | 54 | ### v0.8.1 55 | ``` 56 | 5fc7efdcb1e4a6fbbfbb48eb1c4188d91b38c84f5887c23e475c2d5329132275 darwin86_optional vault-pki-monitor-venafi_optional 57 | 5fc7efdcb1e4a6fbbfbb48eb1c4188d91b38c84f5887c23e475c2d5329132275 darwin86_strict vault-pki-monitor-venafi_strict 58 | a2a81b522a1b3529a628477c02367e0ccff9746a2f151736d59d64896b85b9cd darwin_optional vault-pki-monitor-venafi_optional 59 | a2a81b522a1b3529a628477c02367e0ccff9746a2f151736d59d64896b85b9cd darwin_strict vault-pki-monitor-venafi_strict 60 | 3630018e5210090c28931dc8344b5c5fc42b50fca8c2c57966991923f6320e01 linux86_optional vault-pki-monitor-venafi_optional 61 | 3630018e5210090c28931dc8344b5c5fc42b50fca8c2c57966991923f6320e01 linux86_strict vault-pki-monitor-venafi_strict 62 | a11e4b4f29c7fe646511c2b49138fb83cbe5899efe2302e0db30836f94c7f816 linux_optional vault-pki-monitor-venafi_optional 63 | a11e4b4f29c7fe646511c2b49138fb83cbe5899efe2302e0db30836f94c7f816 linux_strict vault-pki-monitor-venafi_strict 64 | d976efc00b986af5970c0ee8da794c8db7071c6d5fd6675144235bff090a2d2c windows86_optional vault-pki-monitor-venafi_optional.exe 65 | d976efc00b986af5970c0ee8da794c8db7071c6d5fd6675144235bff090a2d2c windows86_strict vault-pki-monitor-venafi_strict.exe 66 | f364007fc58da646a70bf59211b2c4ba315f47f28276616048fece37e7871543 windows_optional vault-pki-monitor-venafi_optional.exe 67 | f364007fc58da646a70bf59211b2c4ba315f47f28276616048fece37e7871543 windows_strict vault-pki-monitor-venafi_strict.exe 68 | ``` 69 | 70 | ### v0.8.0 71 | ``` 72 | 13729a38ba5038b6236bcc6b6a6cc7a5686412bcbbea1ff1768892f03c2047c2 darwin86_optional vault-pki-monitor-venafi_optional 73 | 13729a38ba5038b6236bcc6b6a6cc7a5686412bcbbea1ff1768892f03c2047c2 darwin86_strict vault-pki-monitor-venafi_strict 74 | 090580a6b3c8156b3a3a63c8d78614c22e0bf97902f7cb06389eafb2d2103a97 darwin_optional vault-pki-monitor-venafi_optional 75 | 090580a6b3c8156b3a3a63c8d78614c22e0bf97902f7cb06389eafb2d2103a97 darwin_strict vault-pki-monitor-venafi_strict 76 | 91ad36eccc10a77d4acc23ede392532d9adf88acee3b0cf05d80aa2e17f5ee5d linux86_optional vault-pki-monitor-venafi_optional 77 | 91ad36eccc10a77d4acc23ede392532d9adf88acee3b0cf05d80aa2e17f5ee5d linux86_strict vault-pki-monitor-venafi_strict 78 | 592a340ba56ce3b804bbc2398ba158aaf96465a8619405a3f193048a81ddddd0 linux_optional vault-pki-monitor-venafi_optional 79 | 592a340ba56ce3b804bbc2398ba158aaf96465a8619405a3f193048a81ddddd0 linux_strict vault-pki-monitor-venafi_strict 80 | b5b87db682cbfdf3366cc472ec23ac787b1052035a97d4ef5d0067d19afd4032 windows86_optional vault-pki-monitor-venafi_optional.exe 81 | b5b87db682cbfdf3366cc472ec23ac787b1052035a97d4ef5d0067d19afd4032 windows86_strict vault-pki-monitor-venafi_strict.exe 82 | 347eda31eebf504c7370db5aef94e3c992550d817e64e5509bf90be2e1e78605 windows_optional vault-pki-monitor-venafi_optional.exe 83 | 347eda31eebf504c7370db5aef94e3c992550d817e64e5509bf90be2e1e78605 windows_strict vault-pki-monitor-venafi_strict.exe 84 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | consul: 5 | image: "consul:1.1.0" 6 | hostname: "consul" 7 | command: "agent -dev -client 0.0.0.0" 8 | ports: 9 | - "8400:8400" 10 | - "8500:8500" 11 | - "8600:53/udp" 12 | 13 | vault: 14 | depends_on: 15 | - consul 16 | # Venafi plugin image 17 | #image: venafi/vault-pki-monitor-venafi:build 18 | build: . 19 | hostname: "vault" 20 | links: 21 | - "consul:consul" 22 | environment: 23 | VAULT_ADDR: http://127.0.0.1:8200 24 | TRUST_BUNDLE: /opt/venafi/bundle.pem 25 | ports: 26 | - "8200:8200" 27 | extra_hosts: 28 | - "ha-tpp1.sqlha.com:192.168.6.23" 29 | volumes: 30 | # If you want to use trust bundle file option 31 | - /opt/venafi/bundle.pem:/opt/venafi/bundle.pem 32 | entrypoint: /scripts/wait-for-it.sh -t 20 -h consul -p 8500 -s -- vault server -config=/config/vault-config-with-consul.hcl -log-level=debug 33 | 34 | #TODO: this is a workaround to avoid internal network conflict. Need to find a better solution when the network is not specified in the docker-compose file. 35 | networks: 36 | default: 37 | ipam: 38 | driver: default 39 | config: 40 | - subnet: "192.168.84.1/24" 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Venafi/vault-pki-monitor-venafi 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Venafi/vcert/v4 v4.13.0 7 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef 8 | github.com/fatih/structs v1.1.0 9 | github.com/go-test/deep v1.0.7 10 | github.com/hashicorp/errwrap v1.0.0 11 | github.com/hashicorp/vault v1.5.5 12 | github.com/hashicorp/vault/api v1.0.5-0.20200630205458-1a16f3c699c6 13 | github.com/hashicorp/vault/sdk v0.1.14-0.20201020233143-625c50e68971 14 | github.com/mitchellh/mapstructure v1.3.2 15 | github.com/ryanuber/go-glob v1.0.0 16 | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 17 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 18 | gotest.tools/gotestsum v1.6.1 // indirect 19 | ) 20 | 21 | replace github.com/hashicorp/vault/api => github.com/hashicorp/vault/api v0.0.0-20200718022110-340cc2fa263f 22 | 23 | replace gotest.tools/gotestsum => gotest.tools/gotestsum v0.5.4 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | pki "github.com/Venafi/vault-pki-monitor-venafi/plugin/pki" 5 | "github.com/hashicorp/vault/api" 6 | "github.com/hashicorp/vault/sdk/plugin" 7 | "log" 8 | "os" 9 | ) 10 | 11 | //Plugin config 12 | //TODO: Transfer to normal logger 13 | //Example: 14 | //hclog "github.com/hashicorp/go-hclog" 15 | //logger := hclog.New(&hclog.LoggerOptions{}) 16 | 17 | func main() { 18 | apiClientMeta := &api.PluginAPIClientMeta{} 19 | flags := apiClientMeta.FlagSet() 20 | if err := flags.Parse(os.Args[1:]); err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | tlsConfig := apiClientMeta.GetTLSConfig() 25 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 26 | 27 | if err := plugin.Serve(&plugin.ServeOpts{ 28 | BackendFactoryFunc: pki.Factory, 29 | TLSProviderFunc: tlsProviderFunc, 30 | }); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugin/pki/backend.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/hashicorp/vault/sdk/framework" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | ) 13 | 14 | // Factory creates a new backend implementing the logical.Backend interface 15 | func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 16 | b := Backend(conf) 17 | if err := b.Setup(ctx, conf); err != nil { 18 | return nil, err 19 | } 20 | return b, nil 21 | } 22 | 23 | // Backend returns a new Backend framework struct 24 | func Backend(conf *logical.BackendConfig) *backend { 25 | var b backend 26 | b.Backend = &framework.Backend{ 27 | Help: strings.TrimSpace(backendHelp), 28 | 29 | PathsSpecial: &logical.Paths{ 30 | Unauthenticated: []string{ 31 | "cert/*", 32 | "ca/pem", 33 | "ca_chain", 34 | "ca", 35 | "crl/pem", 36 | "crl", 37 | }, 38 | 39 | LocalStorage: []string{ 40 | "revoked/", 41 | "crl", 42 | "certs/", 43 | }, 44 | 45 | Root: []string{ 46 | "root", 47 | "root/sign-self-issued", 48 | }, 49 | 50 | SealWrapStorage: []string{ 51 | "config/ca_bundle", 52 | "venafi-policy", 53 | }, 54 | }, 55 | 56 | Paths: []*framework.Path{ 57 | pathListRoles(&b), 58 | pathRoles(&b), 59 | pathGenerateRoot(&b), 60 | pathSignIntermediate(&b), 61 | pathSignSelfIssued(&b), 62 | pathDeleteRoot(&b), 63 | pathGenerateIntermediate(&b), 64 | pathSetSignedIntermediate(&b), 65 | pathConfigCA(&b), 66 | pathConfigCRL(&b), 67 | pathConfigURLs(&b), 68 | pathSignVerbatim(&b), 69 | pathSign(&b), 70 | pathIssue(&b), 71 | pathRotateCRL(&b), 72 | pathFetchCA(&b), 73 | pathFetchCAChain(&b), 74 | pathFetchCRL(&b), 75 | pathFetchCRLViaCertPath(&b), 76 | pathFetchValid(&b), 77 | pathFetchListCerts(&b), 78 | pathImportQueue(&b), 79 | pathImportQueueList(&b), 80 | pathVenafiPolicy(&b), 81 | pathVenafiPolicyContent(&b), 82 | pathVenafiPolicyList(&b), 83 | pathVenafiPolicyMap(&b), 84 | pathVenafiPolicySync(&b), 85 | pathRevoke(&b), 86 | pathTidy(&b), 87 | pathVenafiSecrets(&b), 88 | pathVenafiSecretsList(&b), 89 | }, 90 | 91 | Secrets: []*framework.Secret{ 92 | secretCerts(&b), 93 | }, 94 | 95 | BackendType: logical.TypeLogical, 96 | } 97 | 98 | b.crlLifetime = time.Hour * 72 99 | b.tidyCASGuard = new(uint32) 100 | b.storage = conf.StorageView 101 | //Don't start import queue on tests which are using nil storage 102 | if b.storage == nil { 103 | log.Println("Can't start queue when storage is nil") 104 | } else { 105 | b.taskStorage.init() 106 | b.importToTPP(conf) 107 | b.syncRoleWithVenafiPolicyRegister(conf) 108 | } 109 | 110 | return &b 111 | } 112 | 113 | type backend struct { 114 | *framework.Backend 115 | 116 | storage logical.Storage 117 | crlLifetime time.Duration 118 | revokeStorageLock sync.RWMutex 119 | tidyCASGuard *uint32 120 | taskStorage taskStorageStruct 121 | mux sync.Mutex 122 | } 123 | 124 | const backendHelp = ` 125 | The PKI backend dynamically generates X509 server and client certificates. 126 | 127 | After mounting this backend, configure the CA using the "pem_bundle" endpoint within 128 | the "config/" path. 129 | ` 130 | -------------------------------------------------------------------------------- /plugin/pki/ca_util.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/vault/sdk/framework" 7 | "github.com/hashicorp/vault/sdk/logical" 8 | ) 9 | 10 | func (b *backend) getGenerationParams( 11 | data *framework.FieldData, 12 | ) (exported bool, format string, role *roleEntry, errorResp *logical.Response) { 13 | exportedStr := data.Get("exported").(string) 14 | switch exportedStr { 15 | case "exported": 16 | exported = true 17 | case "internal": 18 | default: 19 | errorResp = logical.ErrorResponse( 20 | `the "exported" path parameter must be "internal" or "exported"`) 21 | return 22 | } 23 | 24 | format = getFormat(data) 25 | if format == "" { 26 | errorResp = logical.ErrorResponse( 27 | `the "format" path parameter must be "pem", "der", "der_pkcs", or "pem_bundle"`) 28 | return 29 | } 30 | 31 | role = &roleEntry{ 32 | TTL: time.Duration(data.Get("ttl").(int)) * time.Second, 33 | KeyType: data.Get("key_type").(string), 34 | KeyBits: data.Get("key_bits").(int), 35 | AllowLocalhost: true, 36 | AllowAnyName: true, 37 | AllowIPSANs: true, 38 | EnforceHostnames: false, 39 | AllowedURISANs: []string{"*"}, 40 | AllowedOtherSANs: []string{"*"}, 41 | AllowedSerialNumbers: []string{"*"}, 42 | OU: data.Get("ou").([]string), 43 | Organization: data.Get("organization").([]string), 44 | Country: data.Get("country").([]string), 45 | Locality: data.Get("locality").([]string), 46 | Province: data.Get("province").([]string), 47 | StreetAddress: data.Get("street_address").([]string), 48 | PostalCode: data.Get("postal_code").([]string), 49 | } 50 | 51 | if role.KeyType == "rsa" && role.KeyBits < 2048 { 52 | errorResp = logical.ErrorResponse("RSA keys < 2048 bits are unsafe and not supported") 53 | return 54 | } 55 | 56 | errorResp = validateKeyTypeLength(role.KeyType, role.KeyBits) 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /plugin/pki/cert_util_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "strings" 9 | 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | func TestPki_FetchCertBySerial(t *testing.T) { 14 | storage := &logical.InmemStorage{} 15 | 16 | cases := map[string]struct { 17 | Req *logical.Request 18 | Prefix string 19 | Serial string 20 | }{ 21 | "valid cert": { 22 | &logical.Request{ 23 | Storage: storage, 24 | }, 25 | "certs/", 26 | "00:00:00:00:00:00:00:00", 27 | }, 28 | "revoked cert": { 29 | &logical.Request{ 30 | Storage: storage, 31 | }, 32 | "revoked/", 33 | "11:11:11:11:11:11:11:11", 34 | }, 35 | } 36 | 37 | // Test for colon-based paths in storage 38 | for name, tc := range cases { 39 | storageKey := fmt.Sprintf("%s%s", tc.Prefix, tc.Serial) 40 | err := storage.Put(context.Background(), &logical.StorageEntry{ 41 | Key: storageKey, 42 | Value: []byte("some data"), 43 | }) 44 | if err != nil { 45 | t.Fatalf("error writing to storage on %s colon-based storage path: %s", name, err) 46 | } 47 | 48 | certEntry, err := fetchCertBySerial(context.Background(), tc.Req, tc.Prefix, tc.Serial) 49 | if err != nil { 50 | t.Fatalf("error on %s for colon-based storage path: %s", name, err) 51 | } 52 | 53 | // Check for non-nil on valid/revoked certs 54 | if certEntry == nil { 55 | t.Fatalf("nil on %s for colon-based storage path", name) 56 | } 57 | 58 | // Ensure that cert serials are converted/updated after fetch 59 | expectedKey := tc.Prefix + normalizeSerial(tc.Serial) 60 | se, err := storage.Get(context.Background(), expectedKey) 61 | if err != nil { 62 | t.Fatalf("error on %s for colon-based storage path:%s", name, err) 63 | } 64 | if strings.Compare(expectedKey, se.Key) != 0 { 65 | t.Fatalf("expected: %s, got: %s", expectedKey, certEntry.Key) 66 | } 67 | } 68 | 69 | // Reset storage 70 | storage = &logical.InmemStorage{} 71 | 72 | // Test for hyphen-base paths in storage 73 | for name, tc := range cases { 74 | storageKey := tc.Prefix + normalizeSerial(tc.Serial) 75 | err := storage.Put(context.Background(), &logical.StorageEntry{ 76 | Key: storageKey, 77 | Value: []byte("some data"), 78 | }) 79 | if err != nil { 80 | t.Fatalf("error writing to storage on %s hyphen-based storage path: %s", name, err) 81 | } 82 | 83 | certEntry, err := fetchCertBySerial(context.Background(), tc.Req, tc.Prefix, tc.Serial) 84 | if err != nil || certEntry == nil { 85 | t.Fatalf("error on %s for hyphen-based storage path: err: %v, entry: %v", name, err, certEntry) 86 | } 87 | } 88 | 89 | noConvCases := map[string]struct { 90 | Req *logical.Request 91 | Prefix string 92 | Serial string 93 | }{ 94 | "ca": { 95 | &logical.Request{ 96 | Storage: storage, 97 | }, 98 | "", 99 | "ca", 100 | }, 101 | "crl": { 102 | &logical.Request{ 103 | Storage: storage, 104 | }, 105 | "", 106 | "crl", 107 | }, 108 | } 109 | 110 | // Test for ca and crl case 111 | for name, tc := range noConvCases { 112 | err := storage.Put(context.Background(), &logical.StorageEntry{ 113 | Key: tc.Serial, 114 | Value: []byte("some data"), 115 | }) 116 | if err != nil { 117 | t.Fatalf("error writing to storage on %s: %s", name, err) 118 | } 119 | 120 | certEntry, err := fetchCertBySerial(context.Background(), tc.Req, tc.Prefix, tc.Serial) 121 | if err != nil || certEntry == nil { 122 | t.Fatalf("error on %s: err: %v, entry: %v", name, err, certEntry) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /plugin/pki/crl_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "crypto/x509" 5 | "testing" 6 | 7 | "github.com/hashicorp/vault/api" 8 | vaulthttp "github.com/hashicorp/vault/http" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/hashicorp/vault/vault" 11 | ) 12 | 13 | func TestBackend_CRL_EnableDisable(t *testing.T) { 14 | coreConfig := &vault.CoreConfig{ 15 | LogicalBackends: map[string]logical.Factory{ 16 | "pki": Factory, 17 | }, 18 | } 19 | cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ 20 | HandlerFunc: vaulthttp.Handler, 21 | }) 22 | cluster.Start() 23 | defer cluster.Cleanup() 24 | 25 | client := cluster.Cores[0].Client 26 | var err error 27 | err = client.Sys().Mount("pki", &api.MountInput{ 28 | Type: "pki", 29 | Config: api.MountConfigInput{ 30 | DefaultLeaseTTL: "16h", 31 | MaxLeaseTTL: "60h", 32 | }, 33 | }) 34 | writePolicyToClient("pki", client, t) 35 | resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ 36 | "ttl": "40h", 37 | "common_name": "myvault.com", 38 | }) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | caSerial := resp.Data["serial_number"] 43 | 44 | _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ 45 | "allow_bare_domains": true, 46 | "allow_subdomains": true, 47 | "allowed_domains": "foobar.com", 48 | "generate_lease": true, 49 | }) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | var serials = make(map[int]string) 55 | for i := 0; i < 6; i++ { 56 | resp, err := client.Logical().Write("pki/issue/test", map[string]interface{}{ 57 | "common_name": "test.foobar.com", 58 | }) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | serials[i] = resp.Data["serial_number"].(string) 63 | } 64 | 65 | test := func(num int) { 66 | resp, err := client.Logical().Read("pki/cert/crl") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | crlPem := resp.Data["certificate"].(string) 71 | certList, err := x509.ParseCRL([]byte(crlPem)) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | lenList := len(certList.TBSCertList.RevokedCertificates) 76 | if lenList != num { 77 | t.Fatalf("expected %d, found %d", num, lenList) 78 | } 79 | } 80 | 81 | revoke := func(num int) { 82 | resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{ 83 | "serial_number": serials[num], 84 | }) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{ 90 | "serial_number": caSerial, 91 | }) 92 | if err == nil { 93 | t.Fatal("expected error") 94 | } 95 | } 96 | 97 | toggle := func(disabled bool) { 98 | _, err = client.Logical().Write("pki/config/crl", map[string]interface{}{ 99 | "disable": disabled, 100 | }) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | } 105 | 106 | test(0) 107 | revoke(0) 108 | revoke(1) 109 | test(2) 110 | toggle(true) 111 | test(0) 112 | revoke(2) 113 | revoke(3) 114 | test(0) 115 | toggle(false) 116 | test(4) 117 | revoke(4) 118 | revoke(5) 119 | test(6) 120 | toggle(true) 121 | test(0) 122 | toggle(false) 123 | test(6) 124 | } 125 | -------------------------------------------------------------------------------- /plugin/pki/crl_util.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "errors" 9 | "fmt" 10 | "strings" 11 | "time" 12 | 13 | "github.com/hashicorp/vault/sdk/helper/certutil" 14 | "github.com/hashicorp/vault/sdk/helper/errutil" 15 | "github.com/hashicorp/vault/sdk/logical" 16 | ) 17 | 18 | type revocationInfo struct { 19 | CertificateBytes []byte `json:"certificate_bytes"` 20 | RevocationTime int64 `json:"revocation_time"` 21 | RevocationTimeUTC time.Time `json:"revocation_time_utc"` 22 | } 23 | 24 | // Revokes a cert, and tries to be smart about error recovery 25 | func revokeCert(ctx context.Context, b *backend, req *logical.Request, serial string, fromLease bool) (*logical.Response, error) { 26 | // As this backend is self-contained and this function does not hook into 27 | // third parties to manage users or resources, if the mount is tainted, 28 | // revocation doesn't matter anyways -- the CRL that would be written will 29 | // be immediately blown away by the view being cleared. So we can simply 30 | // fast path a successful exit. 31 | if b.System().Tainted() { 32 | return nil, nil 33 | } 34 | 35 | signingBundle, caErr := fetchCAInfo(ctx, req) 36 | switch caErr.(type) { 37 | case errutil.UserError: 38 | return logical.ErrorResponse(fmt.Sprintf("could not fetch the CA certificate: %s", caErr)), nil 39 | case errutil.InternalError: 40 | return nil, fmt.Errorf("error fetching CA certificate: %s", caErr) 41 | } 42 | if signingBundle == nil { 43 | return nil, errors.New("CA info not found") 44 | } 45 | colonSerial := strings.Replace(strings.ToLower(serial), "-", ":", -1) 46 | if colonSerial == certutil.GetHexFormatted(signingBundle.Certificate.SerialNumber.Bytes(), ":") { 47 | return logical.ErrorResponse("adding CA to CRL is not allowed"), nil 48 | } 49 | 50 | alreadyRevoked := false 51 | var revInfo revocationInfo 52 | 53 | revEntry, err := fetchCertBySerial(ctx, req, "revoked/", serial) 54 | if err != nil { 55 | switch err.(type) { 56 | case errutil.UserError: 57 | return logical.ErrorResponse(err.Error()), nil 58 | case errutil.InternalError: 59 | return nil, err 60 | } 61 | } 62 | if revEntry != nil { 63 | // Set the revocation info to the existing values 64 | alreadyRevoked = true 65 | err = revEntry.DecodeJSON(&revInfo) 66 | if err != nil { 67 | return nil, fmt.Errorf("error decoding existing revocation info") 68 | } 69 | } 70 | 71 | if !alreadyRevoked { 72 | certEntry, err := fetchCertBySerial(ctx, req, "certs/", serial) 73 | if err != nil { 74 | switch err.(type) { 75 | case errutil.UserError: 76 | return logical.ErrorResponse(err.Error()), nil 77 | case errutil.InternalError: 78 | return nil, err 79 | } 80 | } 81 | if certEntry == nil { 82 | return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found", serial)), nil 83 | } 84 | 85 | cert, err := x509.ParseCertificate(certEntry.Value) 86 | if err != nil { 87 | return nil, fmt.Errorf("error parsing certificate: %w", err) 88 | } 89 | if cert == nil { 90 | return nil, fmt.Errorf("got a nil certificate") 91 | } 92 | 93 | // Add a little wiggle room because leases are stored with a second 94 | // granularity 95 | if cert.NotAfter.Before(time.Now().Add(2 * time.Second)) { 96 | return nil, nil 97 | } 98 | 99 | // Compatibility: Don't revoke CAs if they had leases. New CAs going 100 | // forward aren't issued leases. 101 | if cert.IsCA && fromLease { 102 | return nil, nil 103 | } 104 | 105 | currTime := time.Now() 106 | revInfo.CertificateBytes = certEntry.Value 107 | revInfo.RevocationTime = currTime.Unix() 108 | revInfo.RevocationTimeUTC = currTime.UTC() 109 | 110 | revEntry, err = logical.StorageEntryJSON("revoked/"+normalizeSerial(serial), revInfo) 111 | if err != nil { 112 | return nil, fmt.Errorf("error creating revocation entry") 113 | } 114 | 115 | err = req.Storage.Put(ctx, revEntry) 116 | if err != nil { 117 | return nil, fmt.Errorf("error saving revoked certificate to new location") 118 | } 119 | 120 | } 121 | 122 | crlErr := buildCRL(ctx, b, req, false) 123 | switch crlErr.(type) { 124 | case errutil.UserError: 125 | return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil 126 | case errutil.InternalError: 127 | return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr) 128 | } 129 | 130 | resp := &logical.Response{ 131 | Data: map[string]interface{}{ 132 | "revocation_time": revInfo.RevocationTime, 133 | }, 134 | } 135 | if !revInfo.RevocationTimeUTC.IsZero() { 136 | resp.Data["revocation_time_rfc3339"] = revInfo.RevocationTimeUTC.Format(time.RFC3339Nano) 137 | } 138 | return resp, nil 139 | } 140 | 141 | // Builds a CRL by going through the list of revoked certificates and building 142 | // a new CRL with the stored revocation times and serial numbers. 143 | func buildCRL(ctx context.Context, b *backend, req *logical.Request, forceNew bool) error { 144 | crlInfo, err := b.CRL(ctx, req.Storage) 145 | if err != nil { 146 | return errutil.InternalError{Err: fmt.Sprintf("error fetching CRL config information: %s", err)} 147 | } 148 | 149 | crlLifetime := b.crlLifetime 150 | var revokedCerts []pkix.RevokedCertificate 151 | var revInfo revocationInfo 152 | var revokedSerials []string 153 | 154 | if crlInfo != nil { 155 | if crlInfo.Expiry != "" { 156 | crlDur, err := time.ParseDuration(crlInfo.Expiry) 157 | if err != nil { 158 | return errutil.InternalError{Err: fmt.Sprintf("error parsing CRL duration of %s", crlInfo.Expiry)} 159 | } 160 | crlLifetime = crlDur 161 | } 162 | 163 | if crlInfo.Disable { 164 | if !forceNew { 165 | return nil 166 | } 167 | goto WRITE 168 | } 169 | } 170 | 171 | revokedSerials, err = req.Storage.List(ctx, "revoked/") 172 | if err != nil { 173 | return errutil.InternalError{Err: fmt.Sprintf("error fetching list of revoked certs: %s", err)} 174 | } 175 | 176 | for _, serial := range revokedSerials { 177 | revokedEntry, err := req.Storage.Get(ctx, "revoked/"+serial) 178 | if err != nil { 179 | return errutil.InternalError{Err: fmt.Sprintf("unable to fetch revoked cert with serial %s: %s", serial, err)} 180 | } 181 | if revokedEntry == nil { 182 | return errutil.InternalError{Err: fmt.Sprintf("revoked certificate entry for serial %s is nil", serial)} 183 | } 184 | if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 { 185 | // TODO: In this case, remove it and continue? How likely is this to 186 | // happen? Alternately, could skip it entirely, or could implement a 187 | // delete function so that there is a way to remove these 188 | return errutil.InternalError{Err: "found revoked serial but actual certificate is empty"} 189 | } 190 | 191 | err = revokedEntry.DecodeJSON(&revInfo) 192 | if err != nil { 193 | return errutil.InternalError{Err: fmt.Sprintf("error decoding revocation entry for serial %s: %s", serial, err)} 194 | } 195 | 196 | revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) 197 | if err != nil { 198 | return errutil.InternalError{Err: fmt.Sprintf("unable to parse stored revoked certificate with serial %s: %s", serial, err)} 199 | } 200 | 201 | // NOTE: We have to change this to UTC time because the CRL standard 202 | // mandates it but Go will happily encode the CRL without this. 203 | newRevCert := pkix.RevokedCertificate{ 204 | SerialNumber: revokedCert.SerialNumber, 205 | } 206 | if !revInfo.RevocationTimeUTC.IsZero() { 207 | newRevCert.RevocationTime = revInfo.RevocationTimeUTC 208 | } else { 209 | newRevCert.RevocationTime = time.Unix(revInfo.RevocationTime, 0).UTC() 210 | } 211 | revokedCerts = append(revokedCerts, newRevCert) 212 | } 213 | 214 | WRITE: 215 | signingBundle, caErr := fetchCAInfo(ctx, req) 216 | switch caErr.(type) { 217 | case errutil.UserError: 218 | return errutil.UserError{Err: fmt.Sprintf("could not fetch the CA certificate: %s", caErr)} 219 | case errutil.InternalError: 220 | return errutil.InternalError{Err: fmt.Sprintf("error fetching CA certificate: %s", caErr)} 221 | } 222 | 223 | crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime)) 224 | if err != nil { 225 | return errutil.InternalError{Err: fmt.Sprintf("error creating new CRL: %s", err)} 226 | } 227 | 228 | err = req.Storage.Put(ctx, &logical.StorageEntry{ 229 | Key: "crl", 230 | Value: crlBytes, 231 | }) 232 | if err != nil { 233 | return errutil.InternalError{Err: fmt.Sprintf("error storing CRL: %s", err)} 234 | } 235 | 236 | return nil 237 | } 238 | -------------------------------------------------------------------------------- /plugin/pki/fields.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import "github.com/hashicorp/vault/sdk/framework" 4 | 5 | // addIssueAndSignCommonFields adds fields common to both CA and non-CA issuing 6 | // and signing 7 | func addIssueAndSignCommonFields(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema { 8 | fields["exclude_cn_from_sans"] = &framework.FieldSchema{ 9 | Type: framework.TypeBool, 10 | Default: false, 11 | Description: `If true, the Common Name will not be 12 | included in DNS or Email Subject Alternate Names. 13 | Defaults to false (CN is included).`, 14 | } 15 | 16 | fields["format"] = &framework.FieldSchema{ 17 | Type: framework.TypeString, 18 | Default: "pem", 19 | Description: `Format for returned data. Can be "pem", "der", 20 | or "pem_bundle". If "pem_bundle" any private 21 | key and issuing cert will be appended to the 22 | certificate pem. Defaults to "pem".`, 23 | } 24 | 25 | fields["private_key_format"] = &framework.FieldSchema{ 26 | Type: framework.TypeString, 27 | Default: "der", 28 | Description: `Format for the returned private key. 29 | Generally the default will be controlled by the "format" 30 | parameter as either base64-encoded DER or PEM-encoded DER. 31 | However, this can be set to "pkcs8" to have the returned 32 | private key contain base64-encoded pkcs8 or PEM-encoded 33 | pkcs8 instead. Defaults to "der".`, 34 | } 35 | 36 | fields["ip_sans"] = &framework.FieldSchema{ 37 | Type: framework.TypeCommaStringSlice, 38 | Description: `The requested IP SANs, if any, in a 39 | comma-delimited list`, 40 | } 41 | 42 | fields["uri_sans"] = &framework.FieldSchema{ 43 | Type: framework.TypeCommaStringSlice, 44 | Description: `The requested URI SANs, if any, in a 45 | comma-delimited list.`, 46 | } 47 | 48 | fields["other_sans"] = &framework.FieldSchema{ 49 | Type: framework.TypeCommaStringSlice, 50 | Description: `Requested other SANs, in an array with the format 51 | ;UTF8: for each entry.`, 52 | } 53 | 54 | return fields 55 | } 56 | 57 | // addNonCACommonFields adds fields with help text specific to non-CA 58 | // certificate issuing and signing 59 | func addNonCACommonFields(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema { 60 | fields = addIssueAndSignCommonFields(fields) 61 | 62 | fields["role"] = &framework.FieldSchema{ 63 | Type: framework.TypeString, 64 | Description: `The desired role with configuration for this 65 | request`, 66 | } 67 | 68 | fields["common_name"] = &framework.FieldSchema{ 69 | Type: framework.TypeString, 70 | Description: `The requested common name; if you want more than 71 | one, specify the alternative names in the 72 | alt_names map. If email protection is enabled 73 | in the role, this may be an email address.`, 74 | } 75 | 76 | fields["alt_names"] = &framework.FieldSchema{ 77 | Type: framework.TypeString, 78 | Description: `The requested Subject Alternative Names, if any, 79 | in a comma-delimited list. If email protection 80 | is enabled for the role, this may contain 81 | email addresses.`, 82 | } 83 | 84 | fields["serial_number"] = &framework.FieldSchema{ 85 | Type: framework.TypeString, 86 | Description: `The requested serial number, if any. If you want 87 | more than one, specify alternative names in 88 | the alt_names map using OID 2.5.4.5.`, 89 | } 90 | 91 | fields["ttl"] = &framework.FieldSchema{ 92 | Type: framework.TypeDurationSecond, 93 | Description: `The requested Time To Live for the certificate; 94 | sets the expiration date. If not specified 95 | the role default, backend default, or system 96 | default TTL is used, in that order. Cannot 97 | be larger than the role max TTL.`, 98 | } 99 | 100 | return fields 101 | } 102 | 103 | // addCACommonFields adds fields with help text specific to CA 104 | // certificate issuing and signing 105 | func addCACommonFields(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema { 106 | fields = addIssueAndSignCommonFields(fields) 107 | 108 | fields["alt_names"] = &framework.FieldSchema{ 109 | Type: framework.TypeString, 110 | Description: `The requested Subject Alternative Names, if any, 111 | in a comma-delimited list. May contain both 112 | DNS names and email addresses.`, 113 | } 114 | 115 | fields["common_name"] = &framework.FieldSchema{ 116 | Type: framework.TypeString, 117 | Description: `The requested common name; if you want more than 118 | one, specify the alternative names in the alt_names 119 | map. If not specified when signing, the common 120 | name will be taken from the CSR; other names 121 | must still be specified in alt_names or ip_sans.`, 122 | } 123 | 124 | fields["ttl"] = &framework.FieldSchema{ 125 | Type: framework.TypeDurationSecond, 126 | Description: `The requested Time To Live for the certificate; 127 | sets the expiration date. If not specified 128 | the role default, backend default, or system 129 | default TTL is used, in that order. Cannot 130 | be larger than the mount max TTL. Note: 131 | this only has an effect when generating 132 | a CA cert or signing a CA cert, not when 133 | generating a CSR for an intermediate CA.`, 134 | } 135 | 136 | fields["ou"] = &framework.FieldSchema{ 137 | Type: framework.TypeCommaStringSlice, 138 | Description: `If set, OU (OrganizationalUnit) will be set to 139 | this value.`, 140 | } 141 | 142 | fields["organization"] = &framework.FieldSchema{ 143 | Type: framework.TypeCommaStringSlice, 144 | Description: `If set, O (Organization) will be set to 145 | this value.`, 146 | } 147 | 148 | fields["country"] = &framework.FieldSchema{ 149 | Type: framework.TypeCommaStringSlice, 150 | Description: `If set, Country will be set to 151 | this value.`, 152 | } 153 | 154 | fields["locality"] = &framework.FieldSchema{ 155 | Type: framework.TypeCommaStringSlice, 156 | Description: `If set, Locality will be set to 157 | this value.`, 158 | } 159 | 160 | fields["province"] = &framework.FieldSchema{ 161 | Type: framework.TypeCommaStringSlice, 162 | Description: `If set, Province will be set to 163 | this value.`, 164 | } 165 | 166 | fields["street_address"] = &framework.FieldSchema{ 167 | Type: framework.TypeCommaStringSlice, 168 | Description: `If set, Street Address will be set to 169 | this value.`, 170 | } 171 | 172 | fields["postal_code"] = &framework.FieldSchema{ 173 | Type: framework.TypeCommaStringSlice, 174 | Description: `If set, Postal Code will be set to 175 | this value.`, 176 | } 177 | 178 | fields["serial_number"] = &framework.FieldSchema{ 179 | Type: framework.TypeString, 180 | Description: `The requested serial number, if any. If you want 181 | more than one, specify alternative names in 182 | the alt_names map using OID 2.5.4.5.`, 183 | } 184 | 185 | return fields 186 | } 187 | 188 | // addCAKeyGenerationFields adds fields with help text specific to CA key 189 | // generation and exporting 190 | func addCAKeyGenerationFields(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema { 191 | fields["exported"] = &framework.FieldSchema{ 192 | Type: framework.TypeString, 193 | Description: `Must be "internal" or "exported". If set to 194 | "exported", the generated private key will be 195 | returned. This is your *only* chance to retrieve 196 | the private key!`, 197 | } 198 | 199 | fields["key_bits"] = &framework.FieldSchema{ 200 | Type: framework.TypeInt, 201 | Default: 2048, 202 | Description: `The number of bits to use. You will almost 203 | certainly want to change this if you adjust 204 | the key_type.`, 205 | } 206 | 207 | fields["key_type"] = &framework.FieldSchema{ 208 | Type: framework.TypeString, 209 | Default: "rsa", 210 | Description: `The type of key to use; defaults to RSA. "rsa" 211 | and "ec" are the only valid values.`, 212 | } 213 | 214 | return fields 215 | } 216 | 217 | // addCAIssueFields adds fields common to CA issuing, e.g. when returning 218 | // an actual certificate 219 | func addCAIssueFields(fields map[string]*framework.FieldSchema) map[string]*framework.FieldSchema { 220 | fields["max_path_length"] = &framework.FieldSchema{ 221 | Type: framework.TypeInt, 222 | Default: -1, 223 | Description: "The maximum allowable path length", 224 | } 225 | 226 | fields["permitted_dns_domains"] = &framework.FieldSchema{ 227 | Type: framework.TypeCommaStringSlice, 228 | Description: `Domains for which this certificate is allowed to sign or issue child certificates. If set, all DNS names (subject and alt) on child certs must be exact matches or subsets of the given domains (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`, 229 | } 230 | 231 | return fields 232 | } 233 | -------------------------------------------------------------------------------- /plugin/pki/path_config_ca.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/vault/sdk/framework" 8 | "github.com/hashicorp/vault/sdk/helper/certutil" 9 | "github.com/hashicorp/vault/sdk/helper/errutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | func pathConfigCA(b *backend) *framework.Path { 14 | return &framework.Path{ 15 | Pattern: "config/ca", 16 | Fields: map[string]*framework.FieldSchema{ 17 | "pem_bundle": &framework.FieldSchema{ 18 | Type: framework.TypeString, 19 | Description: `PEM-format, concatenated unencrypted 20 | secret key and certificate.`, 21 | }, 22 | }, 23 | 24 | Callbacks: map[logical.Operation]framework.OperationFunc{ 25 | logical.UpdateOperation: b.pathCAWrite, 26 | }, 27 | 28 | HelpSynopsis: pathConfigCAHelpSyn, 29 | HelpDescription: pathConfigCAHelpDesc, 30 | } 31 | } 32 | 33 | func (b *backend) pathCAWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 34 | pemBundle := data.Get("pem_bundle").(string) 35 | 36 | if pemBundle == "" { 37 | return logical.ErrorResponse("'pem_bundle' was empty"), nil 38 | } 39 | 40 | parsedBundle, err := certutil.ParsePEMBundle(pemBundle) 41 | if err != nil { 42 | switch err.(type) { 43 | case errutil.InternalError: 44 | return nil, err 45 | default: 46 | return logical.ErrorResponse(err.Error()), nil 47 | } 48 | } 49 | 50 | if parsedBundle.PrivateKey == nil || 51 | parsedBundle.PrivateKeyType == certutil.UnknownPrivateKey { 52 | return logical.ErrorResponse("private key not found in the PEM bundle"), nil 53 | } 54 | 55 | if parsedBundle.Certificate == nil { 56 | return logical.ErrorResponse("no certificate found in the PEM bundle"), nil 57 | } 58 | 59 | if !parsedBundle.Certificate.IsCA { 60 | return logical.ErrorResponse("the given certificate is not marked for CA use and cannot be used with this backend"), nil 61 | } 62 | 63 | cb, err := parsedBundle.ToCertBundle() 64 | if err != nil { 65 | return nil, fmt.Errorf("error converting raw values into cert bundle: %w", err) 66 | } 67 | 68 | entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) 69 | if err != nil { 70 | return nil, err 71 | } 72 | err = req.Storage.Put(ctx, entry) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | // For ease of later use, also store just the certificate at a known 78 | // location, plus a fresh CRL 79 | entry.Key = "ca" 80 | entry.Value = parsedBundle.CertificateBytes 81 | err = req.Storage.Put(ctx, entry) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | err = buildCRL(ctx, b, req, true) 87 | 88 | return nil, err 89 | } 90 | 91 | const pathConfigCAHelpSyn = ` 92 | Set the CA certificate and private key used for generated credentials. 93 | ` 94 | 95 | const pathConfigCAHelpDesc = ` 96 | This sets the CA information used for credentials generated by this 97 | by this mount. This must be a PEM-format, concatenated unencrypted 98 | secret key and certificate. 99 | 100 | For security reasons, the secret key cannot be retrieved later. 101 | ` 102 | 103 | const pathConfigCAGenerateHelpSyn = ` 104 | Generate a new CA certificate and private key used for signing. 105 | ` 106 | 107 | const pathConfigCAGenerateHelpDesc = ` 108 | This path generates a CA certificate and private key to be used for 109 | credentials generated by this mount. The path can either 110 | end in "internal" or "exported"; this controls whether the 111 | unencrypted private key is exported after generation. This will 112 | be your only chance to export the private key; for security reasons 113 | it cannot be read or exported later. 114 | 115 | If the "type" option is set to "self-signed", the generated 116 | certificate will be a self-signed root CA. Otherwise, this mount 117 | will act as an intermediate CA; a CSR will be returned, to be signed 118 | by your chosen CA (which could be another mount of this backend). 119 | Note that the CRL path will be set to this mount's CRL path; if you 120 | need further customization it is recommended that you create a CSR 121 | separately and get it signed. Either way, use the "config/ca/set" 122 | endpoint to load the signed certificate into Vault. 123 | ` 124 | 125 | const pathConfigCASignHelpSyn = ` 126 | Generate a signed CA certificate from a CSR. 127 | ` 128 | 129 | const pathConfigCASignHelpDesc = ` 130 | This path generates a CA certificate to be used for credentials 131 | generated by the certificate's destination mount. 132 | 133 | Use the "config/ca/set" endpoint to load the signed certificate 134 | into Vault another Vault mount. 135 | ` 136 | -------------------------------------------------------------------------------- /plugin/pki/path_config_crl.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/errutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | // CRLConfig holds basic CRL configuration information 14 | type crlConfig struct { 15 | Expiry string `json:"expiry" mapstructure:"expiry"` 16 | Disable bool `json:"disable"` 17 | } 18 | 19 | func pathConfigCRL(b *backend) *framework.Path { 20 | return &framework.Path{ 21 | Pattern: "config/crl", 22 | Fields: map[string]*framework.FieldSchema{ 23 | "expiry": &framework.FieldSchema{ 24 | Type: framework.TypeString, 25 | Description: `The amount of time the generated CRL should be 26 | valid; defaults to 72 hours`, 27 | Default: "72h", 28 | }, 29 | "disable": &framework.FieldSchema{ 30 | Type: framework.TypeBool, 31 | Description: `If set to true, disables generating the CRL entirely.`, 32 | }, 33 | }, 34 | 35 | Callbacks: map[logical.Operation]framework.OperationFunc{ 36 | logical.ReadOperation: b.pathCRLRead, 37 | logical.UpdateOperation: b.pathCRLWrite, 38 | }, 39 | 40 | HelpSynopsis: pathConfigCRLHelpSyn, 41 | HelpDescription: pathConfigCRLHelpDesc, 42 | } 43 | } 44 | 45 | func (b *backend) CRL(ctx context.Context, s logical.Storage) (*crlConfig, error) { 46 | entry, err := s.Get(ctx, "config/crl") 47 | if err != nil { 48 | return nil, err 49 | } 50 | if entry == nil { 51 | return nil, nil 52 | } 53 | 54 | var result crlConfig 55 | if err := entry.DecodeJSON(&result); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &result, nil 60 | } 61 | 62 | func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 63 | config, err := b.CRL(ctx, req.Storage) 64 | if err != nil { 65 | return nil, err 66 | } 67 | if config == nil { 68 | return nil, nil 69 | } 70 | 71 | return &logical.Response{ 72 | Data: map[string]interface{}{ 73 | "expiry": config.Expiry, 74 | "disable": config.Disable, 75 | }, 76 | }, nil 77 | } 78 | 79 | func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 80 | config, err := b.CRL(ctx, req.Storage) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if config == nil { 85 | config = &crlConfig{} 86 | } 87 | 88 | if expiryRaw, ok := d.GetOk("expiry"); ok { 89 | expiry := expiryRaw.(string) 90 | _, err := time.ParseDuration(expiry) 91 | if err != nil { 92 | return logical.ErrorResponse(fmt.Sprintf("given expiry could not be decoded: %s", err)), nil 93 | } 94 | config.Expiry = expiry 95 | } 96 | 97 | var oldDisable bool 98 | if disableRaw, ok := d.GetOk("disable"); ok { 99 | oldDisable = config.Disable 100 | config.Disable = disableRaw.(bool) 101 | } 102 | 103 | entry, err := logical.StorageEntryJSON("config/crl", config) 104 | if err != nil { 105 | return nil, err 106 | } 107 | err = req.Storage.Put(ctx, entry) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | if oldDisable != config.Disable { 113 | // It wasn't disabled but now it is, rotate 114 | crlErr := buildCRL(ctx, b, req, true) 115 | switch crlErr.(type) { 116 | case errutil.UserError: 117 | return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil 118 | case errutil.InternalError: 119 | return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr) 120 | } 121 | } 122 | 123 | return nil, nil 124 | } 125 | 126 | const pathConfigCRLHelpSyn = ` 127 | Configure the CRL expiration. 128 | ` 129 | 130 | const pathConfigCRLHelpDesc = ` 131 | This endpoint allows configuration of the CRL lifetime. 132 | ` 133 | -------------------------------------------------------------------------------- /plugin/pki/path_config_urls.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/asaskevich/govalidator" 8 | "github.com/fatih/structs" 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | func pathConfigURLs(b *backend) *framework.Path { 14 | return &framework.Path{ 15 | Pattern: "config/urls", 16 | Fields: map[string]*framework.FieldSchema{ 17 | "issuing_certificates": &framework.FieldSchema{ 18 | Type: framework.TypeCommaStringSlice, 19 | Description: `Comma-separated list of URLs to be used 20 | for the issuing certificate attribute`, 21 | }, 22 | 23 | "crl_distribution_points": &framework.FieldSchema{ 24 | Type: framework.TypeCommaStringSlice, 25 | Description: `Comma-separated list of URLs to be used 26 | for the CRL distribution points attribute`, 27 | }, 28 | 29 | "ocsp_servers": &framework.FieldSchema{ 30 | Type: framework.TypeCommaStringSlice, 31 | Description: `Comma-separated list of URLs to be used 32 | for the OCSP servers attribute`, 33 | }, 34 | }, 35 | 36 | Callbacks: map[logical.Operation]framework.OperationFunc{ 37 | logical.UpdateOperation: b.pathWriteURL, 38 | logical.ReadOperation: b.pathReadURL, 39 | }, 40 | 41 | HelpSynopsis: pathConfigURLsHelpSyn, 42 | HelpDescription: pathConfigURLsHelpDesc, 43 | } 44 | } 45 | 46 | func validateURLs(urls []string) string { 47 | for _, curr := range urls { 48 | if !govalidator.IsURL(curr) { 49 | return curr 50 | } 51 | } 52 | 53 | return "" 54 | } 55 | 56 | func getURLs(ctx context.Context, req *logical.Request) (*urlEntries, error) { 57 | entry, err := req.Storage.Get(ctx, "urls") 58 | if err != nil { 59 | return nil, err 60 | } 61 | if entry == nil { 62 | return nil, nil 63 | } 64 | 65 | var entries urlEntries 66 | if err := entry.DecodeJSON(&entries); err != nil { 67 | return nil, err 68 | } 69 | 70 | return &entries, nil 71 | } 72 | 73 | func writeURLs(ctx context.Context, req *logical.Request, entries *urlEntries) error { 74 | entry, err := logical.StorageEntryJSON("urls", entries) 75 | if err != nil { 76 | return err 77 | } 78 | if entry == nil { 79 | return fmt.Errorf("unable to marshal entry into JSON") 80 | } 81 | 82 | err = req.Storage.Put(ctx, entry) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (b *backend) pathReadURL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 91 | entries, err := getURLs(ctx, req) 92 | if err != nil { 93 | return nil, err 94 | } 95 | if entries == nil { 96 | return nil, nil 97 | } 98 | 99 | resp := &logical.Response{ 100 | Data: structs.New(entries).Map(), 101 | } 102 | 103 | return resp, nil 104 | } 105 | 106 | func (b *backend) pathWriteURL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 107 | entries, err := getURLs(ctx, req) 108 | if err != nil { 109 | return nil, err 110 | } 111 | if entries == nil { 112 | entries = &urlEntries{ 113 | IssuingCertificates: []string{}, 114 | CRLDistributionPoints: []string{}, 115 | OCSPServers: []string{}, 116 | } 117 | } 118 | 119 | if urlsInt, ok := data.GetOk("issuing_certificates"); ok { 120 | entries.IssuingCertificates = urlsInt.([]string) 121 | if badURL := validateURLs(entries.IssuingCertificates); badURL != "" { 122 | return logical.ErrorResponse(fmt.Sprintf( 123 | "invalid URL found in issuing certificates: %s", badURL)), nil 124 | } 125 | } 126 | if urlsInt, ok := data.GetOk("crl_distribution_points"); ok { 127 | entries.CRLDistributionPoints = urlsInt.([]string) 128 | if badURL := validateURLs(entries.CRLDistributionPoints); badURL != "" { 129 | return logical.ErrorResponse(fmt.Sprintf( 130 | "invalid URL found in CRL distribution points: %s", badURL)), nil 131 | } 132 | } 133 | if urlsInt, ok := data.GetOk("ocsp_servers"); ok { 134 | entries.OCSPServers = urlsInt.([]string) 135 | if badURL := validateURLs(entries.OCSPServers); badURL != "" { 136 | return logical.ErrorResponse(fmt.Sprintf( 137 | "invalid URL found in OCSP servers: %s", badURL)), nil 138 | } 139 | } 140 | 141 | return nil, writeURLs(ctx, req, entries) 142 | } 143 | 144 | type urlEntries struct { 145 | IssuingCertificates []string `json:"issuing_certificates" structs:"issuing_certificates" mapstructure:"issuing_certificates"` 146 | CRLDistributionPoints []string `json:"crl_distribution_points" structs:"crl_distribution_points" mapstructure:"crl_distribution_points"` 147 | OCSPServers []string `json:"ocsp_servers" structs:"ocsp_servers" mapstructure:"ocsp_servers"` 148 | } 149 | 150 | const pathConfigURLsHelpSyn = ` 151 | Set the URLs for the issuing CA, CRL distribution points, and OCSP servers. 152 | ` 153 | 154 | const pathConfigURLsHelpDesc = ` 155 | This path allows you to set the issuing CA, CRL distribution points, and 156 | OCSP server URLs that will be encoded into issued certificates. If these 157 | values are not set, no such information will be encoded in the issued 158 | certificates. To delete URLs, simply re-set the appropriate value with an 159 | empty string. 160 | 161 | Multiple URLs can be specified for each type; use commas to separate them. 162 | ` 163 | -------------------------------------------------------------------------------- /plugin/pki/path_fetch.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "encoding/pem" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/helper/errutil" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | ) 13 | 14 | // Returns the CA in raw format 15 | func pathFetchCA(b *backend) *framework.Path { 16 | return &framework.Path{ 17 | Pattern: `ca(/pem)?`, 18 | 19 | Callbacks: map[logical.Operation]framework.OperationFunc{ 20 | logical.ReadOperation: b.pathFetchRead, 21 | }, 22 | 23 | HelpSynopsis: pathFetchHelpSyn, 24 | HelpDescription: pathFetchHelpDesc, 25 | } 26 | } 27 | 28 | // Returns the CA chain 29 | func pathFetchCAChain(b *backend) *framework.Path { 30 | return &framework.Path{ 31 | Pattern: `(cert/)?ca_chain`, 32 | 33 | Callbacks: map[logical.Operation]framework.OperationFunc{ 34 | logical.ReadOperation: b.pathFetchRead, 35 | }, 36 | 37 | HelpSynopsis: pathFetchHelpSyn, 38 | HelpDescription: pathFetchHelpDesc, 39 | } 40 | } 41 | 42 | // Returns the CRL in raw format 43 | func pathFetchCRL(b *backend) *framework.Path { 44 | return &framework.Path{ 45 | Pattern: `crl(/pem)?`, 46 | 47 | Callbacks: map[logical.Operation]framework.OperationFunc{ 48 | logical.ReadOperation: b.pathFetchRead, 49 | }, 50 | 51 | HelpSynopsis: pathFetchHelpSyn, 52 | HelpDescription: pathFetchHelpDesc, 53 | } 54 | } 55 | 56 | // Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path 57 | // also handles returning the CA cert in a non-raw format. 58 | func pathFetchValid(b *backend) *framework.Path { 59 | return &framework.Path{ 60 | Pattern: `cert/(?P[0-9A-Fa-f-:]+)`, 61 | Fields: map[string]*framework.FieldSchema{ 62 | "serial": &framework.FieldSchema{ 63 | Type: framework.TypeString, 64 | Description: `Certificate serial number, in colon- or 65 | hyphen-separated octal`, 66 | }, 67 | }, 68 | 69 | Callbacks: map[logical.Operation]framework.OperationFunc{ 70 | logical.ReadOperation: b.pathFetchRead, 71 | }, 72 | 73 | HelpSynopsis: pathFetchHelpSyn, 74 | HelpDescription: pathFetchHelpDesc, 75 | } 76 | } 77 | 78 | // This returns the CRL in a non-raw format 79 | func pathFetchCRLViaCertPath(b *backend) *framework.Path { 80 | return &framework.Path{ 81 | Pattern: `cert/crl`, 82 | 83 | Callbacks: map[logical.Operation]framework.OperationFunc{ 84 | logical.ReadOperation: b.pathFetchRead, 85 | }, 86 | 87 | HelpSynopsis: pathFetchHelpSyn, 88 | HelpDescription: pathFetchHelpDesc, 89 | } 90 | } 91 | 92 | // This returns the list of serial numbers for certs 93 | func pathFetchListCerts(b *backend) *framework.Path { 94 | return &framework.Path{ 95 | Pattern: "certs/?$", 96 | 97 | Callbacks: map[logical.Operation]framework.OperationFunc{ 98 | logical.ListOperation: b.pathFetchCertList, 99 | }, 100 | 101 | HelpSynopsis: pathFetchHelpSyn, 102 | HelpDescription: pathFetchHelpDesc, 103 | } 104 | } 105 | 106 | func (b *backend) pathFetchCertList(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { 107 | entries, err := req.Storage.List(ctx, "certs/") 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return logical.ListResponse(entries), nil 113 | } 114 | 115 | func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { 116 | var serial, pemType, contentType string 117 | var certEntry, revokedEntry *logical.StorageEntry 118 | var funcErr error 119 | var certificate []byte 120 | var revocationTime int64 121 | response = &logical.Response{ 122 | Data: map[string]interface{}{}, 123 | } 124 | 125 | // Some of these need to return raw and some non-raw; 126 | // this is basically handled by setting contentType or not. 127 | // Errors don't cause an immediate exit, because the raw 128 | // paths still need to return raw output. 129 | 130 | switch { 131 | case req.Path == "ca" || req.Path == "ca/pem": 132 | serial = "ca" 133 | contentType = "application/pkix-cert" 134 | if req.Path == "ca/pem" { 135 | pemType = "CERTIFICATE" 136 | } 137 | case req.Path == "ca_chain" || req.Path == "cert/ca_chain": 138 | serial = "ca_chain" 139 | if req.Path == "ca_chain" { 140 | contentType = "application/pkix-cert" 141 | } 142 | case req.Path == "crl" || req.Path == "crl/pem": 143 | serial = "crl" 144 | contentType = "application/pkix-crl" 145 | if req.Path == "crl/pem" { 146 | pemType = "X509 CRL" 147 | } 148 | case req.Path == "cert/crl": 149 | serial = "crl" 150 | pemType = "X509 CRL" 151 | default: 152 | serial = data.Get("serial").(string) 153 | pemType = "CERTIFICATE" 154 | } 155 | if len(serial) == 0 { 156 | response = logical.ErrorResponse("The serial number must be provided") 157 | goto reply 158 | } 159 | 160 | if serial == "ca_chain" { 161 | caInfo, err := fetchCAInfo(ctx, req) 162 | switch err.(type) { 163 | case errutil.UserError: 164 | response = logical.ErrorResponse(err.Error()) 165 | goto reply 166 | case errutil.InternalError: 167 | retErr = err 168 | goto reply 169 | } 170 | 171 | caChain := caInfo.GetCAChain() 172 | var certStr string 173 | for _, ca := range caChain { 174 | block := pem.Block{ 175 | Type: "CERTIFICATE", 176 | Bytes: ca.Bytes, 177 | } 178 | certStr = strings.Join([]string{certStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n") 179 | } 180 | certificate = []byte(strings.TrimSpace(certStr)) 181 | goto reply 182 | } 183 | 184 | certEntry, funcErr = fetchCertBySerial(ctx, req, req.Path, serial) 185 | if funcErr != nil { 186 | switch funcErr.(type) { 187 | case errutil.UserError: 188 | response = logical.ErrorResponse(funcErr.Error()) 189 | goto reply 190 | case errutil.InternalError: 191 | retErr = funcErr 192 | goto reply 193 | } 194 | } 195 | if certEntry == nil { 196 | response = nil 197 | goto reply 198 | } 199 | 200 | certificate = certEntry.Value 201 | 202 | if len(pemType) != 0 { 203 | block := pem.Block{ 204 | Type: pemType, 205 | Bytes: certEntry.Value, 206 | } 207 | // This is convoluted on purpose to ensure that we don't have trailing 208 | // newlines via various paths 209 | certificate = []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block)))) 210 | } 211 | 212 | revokedEntry, funcErr = fetchCertBySerial(ctx, req, "revoked/", serial) 213 | if funcErr != nil { 214 | switch funcErr.(type) { 215 | case errutil.UserError: 216 | response = logical.ErrorResponse(funcErr.Error()) 217 | goto reply 218 | case errutil.InternalError: 219 | retErr = funcErr 220 | goto reply 221 | } 222 | } 223 | if revokedEntry != nil { 224 | var revInfo revocationInfo 225 | err := revokedEntry.DecodeJSON(&revInfo) 226 | if err != nil { 227 | return logical.ErrorResponse(fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)), nil 228 | } 229 | revocationTime = revInfo.RevocationTime 230 | } 231 | 232 | reply: 233 | switch { 234 | case len(contentType) != 0: 235 | response = &logical.Response{ 236 | Data: map[string]interface{}{ 237 | logical.HTTPContentType: contentType, 238 | logical.HTTPRawBody: certificate, 239 | }} 240 | if retErr != nil { 241 | if b.Logger().IsWarn() { 242 | b.Logger().Warn("possible error, but cannot return in raw response. Note that an empty CA probably means none was configured, and an empty CRL is possibly correct", "error", retErr) 243 | } 244 | } 245 | retErr = nil 246 | if len(certificate) > 0 { 247 | response.Data[logical.HTTPStatusCode] = 200 248 | } else { 249 | response.Data[logical.HTTPStatusCode] = 204 250 | } 251 | case retErr != nil: 252 | response = nil 253 | return 254 | case response == nil: 255 | return 256 | case response.IsError(): 257 | return response, nil 258 | default: 259 | response.Data["certificate"] = string(certificate) 260 | response.Data["revocation_time"] = revocationTime 261 | } 262 | 263 | return 264 | } 265 | 266 | const pathFetchHelpSyn = ` 267 | Fetch a CA, CRL, CA Chain, or non-revoked certificate. 268 | ` 269 | 270 | const pathFetchHelpDesc = ` 271 | This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched. 272 | 273 | Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. 274 | 275 | Using "ca_chain" as the value fetches the certificate authority trust chain in PEM encoding. 276 | ` 277 | -------------------------------------------------------------------------------- /plugin/pki/path_import_queue.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | "github.com/Venafi/vcert/v4/pkg/certificate" 10 | "github.com/Venafi/vcert/v4/pkg/endpoint" 11 | "github.com/Venafi/vcert/v4/pkg/verror" 12 | "github.com/hashicorp/vault/sdk/framework" 13 | hconsts "github.com/hashicorp/vault/sdk/helper/consts" 14 | "github.com/hashicorp/vault/sdk/logical" 15 | "log" 16 | "regexp" 17 | "strings" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | //Jobs tructure for import queue worker 23 | type Job struct { 24 | id int 25 | entry string 26 | roleName string 27 | policyName string 28 | importPath string 29 | ctx context.Context 30 | //req *logical.Request 31 | storage *logical.Storage 32 | importOnlyNonCompliant bool 33 | } 34 | 35 | // This returns the list of queued for import to TPP certificates 36 | func pathImportQueue(b *backend) *framework.Path { 37 | ret := &framework.Path{ 38 | Pattern: "import-queue/" + framework.GenericNameRegex("role"), 39 | 40 | Callbacks: map[logical.Operation]framework.OperationFunc{ 41 | logical.ReadOperation: b.pathUpdateImportQueue, 42 | //TODO: add delete operation to stop import queue and delete it 43 | //TODO: add delete operation to delete particular import record 44 | 45 | }, 46 | 47 | HelpSynopsis: pathImportQueueSyn, 48 | HelpDescription: pathImportQueueDesc, 49 | } 50 | ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) 51 | return ret 52 | } 53 | 54 | func pathImportQueueList(b *backend) *framework.Path { 55 | ret := &framework.Path{ 56 | Pattern: "import-queue/", 57 | Callbacks: map[logical.Operation]framework.OperationFunc{ 58 | logical.ListOperation: b.pathFetchImportQueueList, 59 | }, 60 | 61 | HelpSynopsis: pathImportQueueSyn, 62 | HelpDescription: pathImportQueueDesc, 63 | } 64 | return ret 65 | } 66 | 67 | func (b *backend) pathFetchImportQueueList(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { 68 | roles, err := req.Storage.List(ctx, "import-queue/") 69 | var entries []string 70 | if err != nil { 71 | return nil, err 72 | } 73 | for _, role := range roles { 74 | log.Printf("%s Getting entry %s", logPrefixVenafiImport, role) 75 | rawEntry, err := req.Storage.List(ctx, "import-queue/"+role) 76 | if err != nil { 77 | return nil, err 78 | } 79 | var entry []string 80 | for _, e := range rawEntry { 81 | entry = append(entry, fmt.Sprintf("%s: %s", role, e)) 82 | } 83 | entries = append(entries, entry...) 84 | } 85 | return logical.ListResponse(entries), nil 86 | } 87 | 88 | func (b *backend) pathUpdateImportQueue(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { 89 | roleName := data.Get("role").(string) 90 | log.Printf("%s Using role: %s", logPrefixVenafiImport, roleName) 91 | 92 | entries, err := req.Storage.List(ctx, "import-queue/"+data.Get("role").(string)+"/") 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return logical.ListResponse(entries), nil 98 | } 99 | 100 | func (b *backend) fillImportQueueTask(roleName string, policyName string, noOfWorkers int, storage logical.Storage, importOnlyNonCompliant bool, conf *logical.BackendConfig) { 101 | ctx := context.Background() 102 | jobs := make(chan Job, 100) 103 | replicationState := conf.System.ReplicationState() 104 | //Checking if we are on master or on the stanby Vault server 105 | isSlave := !(conf.System.LocalMount() || !replicationState.HasState(hconsts.ReplicationPerformanceSecondary)) || 106 | replicationState.HasState(hconsts.ReplicationDRSecondary) || 107 | replicationState.HasState(hconsts.ReplicationPerformanceStandby) 108 | if isSlave { 109 | log.Printf("%s We're on slave. Sleeping", logPrefixVenafiImport) 110 | return 111 | } 112 | log.Printf("%s We're on master. Starting to import certificates", logPrefixVenafiImport) 113 | //var err error 114 | importPath := "import-queue/" + roleName + "/" 115 | 116 | entries, err := storage.List(ctx, importPath) 117 | if err != nil { 118 | log.Printf("%s Could not get queue list from path %s: %s", logPrefixVenafiImport, err, importPath) 119 | return 120 | } 121 | log.Printf("%s Queue list on path %s has length %v", logPrefixVenafiImport, importPath, len(entries)) 122 | 123 | var wg sync.WaitGroup 124 | wg.Add(noOfWorkers) 125 | for i := 0; i < noOfWorkers; i++ { 126 | go func() { 127 | defer func() { 128 | r := recover() 129 | if r != nil { 130 | log.Printf("%s recover %s", logPrefixVenafiImport, r) 131 | } 132 | wg.Done() 133 | }() 134 | for job := range jobs { 135 | result := b.processImportToTPP(job) 136 | log.Printf("%s Job id: %d ### Processed entry: %s , result:\n %v\n", logPrefixVenafiImport, job.id, job.entry, result) 137 | } 138 | }() 139 | } 140 | for i, entry := range entries { 141 | log.Printf("%s Allocating job for entry %s", logPrefixVenafiImport, entry) 142 | job := Job{ 143 | id: i, 144 | entry: entry, 145 | importPath: importPath, 146 | roleName: roleName, 147 | policyName: policyName, 148 | storage: &storage, 149 | ctx: ctx, 150 | importOnlyNonCompliant: importOnlyNonCompliant, 151 | } 152 | jobs <- job 153 | } 154 | close(jobs) 155 | wg.Wait() 156 | } 157 | 158 | func (b *backend) importToTPP(conf *logical.BackendConfig) { 159 | 160 | log.Printf("%s starting importcontroler", logPrefixVenafiImport) 161 | b.taskStorage.register("importcontroler", func() { 162 | b.controlImportQueue(conf) 163 | }, 1, time.Second*1) 164 | } 165 | 166 | func (b *backend) controlImportQueue(conf *logical.BackendConfig) { 167 | log.Printf("%s running control import queue", logPrefixVenafiImport) 168 | ctx := context.Background() 169 | const fillQueuePrefix = "fillqueue-" 170 | roles, err := b.storage.List(ctx, "role/") 171 | if err != nil { 172 | log.Printf("%s Couldn't get list of roles %s", logPrefixVenafiImport, err) 173 | return 174 | } 175 | 176 | policyMap, err := getPolicyRoleMap(ctx, b.storage) 177 | if err != nil { 178 | log.Printf("Can't get policy map: %s", err) 179 | return 180 | } 181 | 182 | for i := range roles { 183 | roleName := roles[i] 184 | if policyMap.Roles[roleName].ImportPolicy == "" { 185 | //no import policy defined for role. Skipping 186 | continue 187 | } 188 | 189 | //Update role since it's settings may be changed 190 | role, err := b.getRole(ctx, b.storage, roleName) 191 | if err != nil { 192 | log.Printf("%s Error getting role %v: %s\n Exiting.", logPrefixVenafiImport, role, err) 193 | continue 194 | } 195 | 196 | if role == nil { 197 | log.Printf("%s Unknown role %v\n", logPrefixVenafiImport, role) 198 | continue 199 | } 200 | 201 | policyConfig, err := b.getVenafiPolicyConfig(ctx, &b.storage, policyMap.Roles[roleName].ImportPolicy) 202 | if err != nil || policyConfig == nil { 203 | log.Printf("%s Error getting policy %v: %v\n Exiting.", logPrefixVenafiImport, policyMap.Roles[roleName].ImportPolicy, err) 204 | continue 205 | } 206 | b.taskStorage.register(fillQueuePrefix+roleName, func() { 207 | log.Printf("%s run queue filler %s", logPrefixVenafiImport, roleName) 208 | //get the policy config here, since this is on the scoupe of this anonymous methods, this will 209 | //solve an issue with the ImportOnlyNonCompliant that doesn't hold the correct value. 210 | policyConfig, _ := b.getVenafiPolicyConfig(ctx, &b.storage, policyMap.Roles[roleName].ImportPolicy) 211 | b.fillImportQueueTask(roleName, policyMap.Roles[roleName].ImportPolicy, policyConfig.VenafiImportWorkers, b.storage, policyConfig.ImportOnlyNonCompliant, conf) 212 | }, 1, time.Duration(policyConfig.VenafiImportTimeout)*time.Second) 213 | 214 | } 215 | stringInSlice := func(s string, sl []string) bool { 216 | for i := range sl { 217 | if sl[i] == s { 218 | return true 219 | } 220 | } 221 | return false 222 | } 223 | for _, taskName := range b.taskStorage.getTasksNames() { 224 | if strings.HasPrefix(taskName, fillQueuePrefix) && !stringInSlice(strings.TrimPrefix(taskName, fillQueuePrefix), roles) { 225 | b.taskStorage.del(taskName) 226 | } 227 | } 228 | log.Printf("%s finished running control import queue", logPrefixVenafiImport) 229 | } 230 | 231 | func (b *backend) processImportToTPP(job Job) string { 232 | 233 | msg := fmt.Sprintf("Job id: %v ###", job.id) 234 | importPath := job.importPath 235 | log.Printf("%s %s Trying to import certificate with SN %s", logPrefixVenafiImport, msg, job.entry) 236 | cl, err := b.ClientVenafi(job.ctx, job.storage, job.policyName) 237 | if err != nil { 238 | return fmt.Sprintf("%s Could not create venafi client: %s", msg, err) 239 | } 240 | 241 | certEntry, err := (*job.storage).Get(job.ctx, importPath+job.entry) 242 | if err != nil { 243 | return fmt.Sprintf("%s Could not get certificate from %s: %s", msg, importPath+job.entry, err) 244 | } 245 | if certEntry == nil { 246 | return fmt.Sprintf("%s Could not get certificate from %s: cert entry not found", msg, importPath+job.entry) 247 | } 248 | block := pem.Block{ 249 | Type: "CERTIFICATE", 250 | Bytes: certEntry.Value, 251 | } 252 | 253 | Certificate, err := x509.ParseCertificate(certEntry.Value) 254 | if err != nil { 255 | return fmt.Sprintf("%s Could not get certificate from entry %s: %s", msg, importPath+job.entry, err) 256 | } 257 | if job.importOnlyNonCompliant { 258 | valid, err := b.checkCertMatchPolicy(Certificate, job.policyName) 259 | if err != nil { 260 | return fmt.Sprintf("Failed checking certificate compliance with policies: %v", err) 261 | } 262 | if valid { 263 | b.deleteCertFromQueue(job) 264 | return fmt.Sprintf("Skipped import of compliant certificate %v for role %v", job.entry, job.roleName) 265 | } 266 | } 267 | 268 | //TODO: here we should check for existing CN and set it to DNS or throw error 269 | cn := Certificate.Subject.CommonName 270 | 271 | certString := string(pem.EncodeToMemory(&block)) 272 | log.Printf("%s %s Importing cert to %s:\n", logPrefixVenafiImport, msg, cn) 273 | 274 | importReq := &certificate.ImportRequest{ 275 | // if PolicyDN is empty, it is taken from cfg.Zone 276 | ObjectName: cn, 277 | CertificateData: certString, 278 | PrivateKeyData: "", 279 | Password: "", 280 | Reconcile: false, 281 | CustomFields: []certificate.CustomField{{Type: certificate.CustomFieldOrigin, Value: "HashiCorp Vault"}}, 282 | } 283 | importResp, err := cl.ImportCertificate(importReq) 284 | if err != nil { 285 | if errors.Is(err, verror.ServerBadDataResponce) || errors.Is(err, verror.UserDataError) { 286 | //TODO: Here should be renew instead of deletion 287 | b.deleteCertFromQueue(job) 288 | } 289 | 290 | /// 291 | if (err != nil) && (cl.GetType() == endpoint.ConnectorTypeTPP) { 292 | msg := err.Error() 293 | 294 | //catch the scenario when token is expired and deleted. 295 | var regex = regexp.MustCompile("(expired|invalid)_token") 296 | 297 | //validate if the error is related to a expired access token, at this moment the only way can validate this is using the error message 298 | //and verify if that message describes errors related to expired access token. 299 | code := getStatusCode(msg) 300 | if code == HTTP_UNAUTHORIZED && regex.MatchString(msg) { 301 | 302 | cfg, err := b.getConfig(job.ctx, job.storage, job.policyName) 303 | 304 | if err != nil { 305 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 306 | } 307 | 308 | if cfg.Credentials.RefreshToken != "" { 309 | msg := fmt.Sprintf("Token will be updated by the Job with id: %v ###", job.id) 310 | b.Logger().Debug(msg) 311 | err = synchronizedUpdateAccessToken(cfg, b, job.ctx, job.storage, job.policyName) 312 | 313 | if err != nil { 314 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 315 | } 316 | 317 | //everything went fine so get the new client with the new refreshed access token 318 | cl, err := b.ClientVenafi(job.ctx, job.storage, job.policyName) 319 | if err != nil { 320 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 321 | } 322 | 323 | b.Logger().Debug("Reading policy configuration again") 324 | 325 | importResp, err = cl.ImportCertificate(importReq) 326 | if err != nil { 327 | if errors.Is(err, verror.ServerBadDataResponce) || errors.Is(err, verror.UserDataError) { 328 | //remove this from current queue, since this is because probably certificate exist on server 329 | //or user feed certificate with invalid data. 330 | b.deleteCertFromQueue(job) 331 | } 332 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 333 | } else { 334 | log.Printf("%s %s Certificate imported:\n %s", logPrefixVenafiImport, msg, pp(importResp)) 335 | b.deleteCertFromQueue(job) 336 | return pp(importResp) 337 | } 338 | } else { 339 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 340 | } 341 | 342 | } else { 343 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 344 | } 345 | } 346 | /// 347 | 348 | return fmt.Sprintf("%s could not import certificate: %s\n", msg, err) 349 | 350 | } 351 | log.Printf("%s %s Certificate imported:\n %s", logPrefixVenafiImport, msg, pp(importResp)) 352 | b.deleteCertFromQueue(job) 353 | return pp(importResp) 354 | 355 | } 356 | 357 | func (b *backend) checkCertMatchPolicy(cert *x509.Certificate, policyName string) (bool, error) { 358 | var req x509.CertificateRequest 359 | req.Subject = cert.Subject 360 | req.Extensions = cert.Extensions 361 | req.PublicKey = cert.PublicKey 362 | req.EmailAddresses = cert.EmailAddresses 363 | req.DNSNames = cert.DNSNames 364 | req.IPAddresses = cert.IPAddresses 365 | req.URIs = cert.URIs 366 | 367 | 368 | 369 | entry, err := b.storage.Get(context.Background(), venafiPolicyPath+policyName+"/policy") 370 | if err != nil { 371 | return false, err 372 | } 373 | if entry == nil { 374 | return false, fmt.Errorf("policy data is nil. You need configure Venafi policy to proceed") 375 | } 376 | 377 | var policy venafiPolicyEntry 378 | 379 | if err := entry.DecodeJSON(&policy); err != nil { 380 | log.Printf("%s error reading Venafi policy configuration: %s", logPrefixVenafiPolicyEnforcement, err) 381 | return false, err 382 | } 383 | err = checkCSR(false, &req, policy) 384 | if err != nil { 385 | return false, nil 386 | } 387 | return true, nil 388 | } 389 | 390 | func (b *backend) deleteCertFromQueue(job Job) { 391 | 392 | msg := fmt.Sprintf("Job id: %v ###", job.id) 393 | importPath := job.importPath 394 | log.Printf("%s %s Removing certificate from import path %s", logPrefixVenafiImport, msg, importPath+job.entry) 395 | err := (*job.storage).Delete(job.ctx, importPath+job.entry) 396 | if err != nil { 397 | log.Printf("%s %s Could not delete %s from queue: %s", logPrefixVenafiImport, msg, importPath+job.entry, err) 398 | } else { 399 | log.Printf("%s %s Certificate with SN %s removed from queue", logPrefixVenafiImport, msg, job.entry) 400 | _, err := (*job.storage).List(job.ctx, importPath) 401 | if err != nil { 402 | log.Printf("%s %s Could not get queue list: %s", logPrefixVenafiImport, msg, err) 403 | } 404 | } 405 | } 406 | 407 | func (b *backend) cleanupImportToTPP(roleName string, ctx context.Context, req *logical.Request) { 408 | 409 | importPath := "import-queue/" + roleName + "/" 410 | entries, err := req.Storage.List(ctx, importPath) 411 | if err != nil { 412 | log.Printf("%s Could not read from queue: %s", logPrefixVenafiImport, err) 413 | } 414 | for _, sn := range entries { 415 | err = req.Storage.Delete(ctx, importPath+sn) 416 | if err != nil { 417 | log.Printf("%s Could not delete %s from queue: %s", logPrefixVenafiImport, importPath+sn, err) 418 | } else { 419 | log.Printf("%s Deleted %s from queue", logPrefixVenafiImport, importPath+sn) 420 | } 421 | } 422 | 423 | } 424 | 425 | const pathImportQueueSyn = ` 426 | Fetch a CA, CRL, CA Chain, or non-revoked certificate. 427 | ` 428 | 429 | const pathImportQueueDesc = ` 430 | This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched. 431 | 432 | Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. 433 | 434 | Using "ca_chain" as the value fetches the certificate authority trust chain in PEM encoding. 435 | ` 436 | -------------------------------------------------------------------------------- /plugin/pki/path_intermediate.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/certutil" 10 | "github.com/hashicorp/vault/sdk/helper/errutil" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | ) 13 | 14 | func pathGenerateIntermediate(b *backend) *framework.Path { 15 | ret := &framework.Path{ 16 | Pattern: "intermediate/generate/" + framework.GenericNameRegex("exported"), 17 | 18 | Callbacks: map[logical.Operation]framework.OperationFunc{ 19 | logical.UpdateOperation: b.pathGenerateIntermediate, 20 | }, 21 | 22 | HelpSynopsis: pathGenerateIntermediateHelpSyn, 23 | HelpDescription: pathGenerateIntermediateHelpDesc, 24 | } 25 | 26 | ret.Fields = addCACommonFields(map[string]*framework.FieldSchema{}) 27 | ret.Fields = addCAKeyGenerationFields(ret.Fields) 28 | ret.Fields["add_basic_constraints"] = &framework.FieldSchema{ 29 | Type: framework.TypeBool, 30 | Description: `Whether to add a Basic Constraints 31 | extension with CA: true. Only needed as a 32 | workaround in some compatibility scenarios 33 | with Active Directory Certificate Services.`, 34 | } 35 | 36 | return ret 37 | } 38 | 39 | func pathSetSignedIntermediate(b *backend) *framework.Path { 40 | ret := &framework.Path{ 41 | Pattern: "intermediate/set-signed", 42 | 43 | Fields: map[string]*framework.FieldSchema{ 44 | "certificate": &framework.FieldSchema{ 45 | Type: framework.TypeString, 46 | Description: `PEM-format certificate. This must be a CA 47 | certificate with a public key matching the 48 | previously-generated key from the generation 49 | endpoint.`, 50 | }, 51 | }, 52 | 53 | Callbacks: map[logical.Operation]framework.OperationFunc{ 54 | logical.UpdateOperation: b.pathSetSignedIntermediate, 55 | }, 56 | 57 | HelpSynopsis: pathSetSignedIntermediateHelpSyn, 58 | HelpDescription: pathSetSignedIntermediateHelpDesc, 59 | } 60 | 61 | return ret 62 | } 63 | 64 | func (b *backend) pathGenerateIntermediate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 65 | var err error 66 | 67 | exported, format, role, errorResp := b.getGenerationParams(data) 68 | if errorResp != nil { 69 | return errorResp, nil 70 | } 71 | 72 | var resp *logical.Response 73 | input := &dataBundle{ 74 | role: role, 75 | req: req, 76 | apiData: data, 77 | } 78 | parsedBundle, err := generateIntermediateCSR(b, input) 79 | if err != nil { 80 | switch err.(type) { 81 | case errutil.UserError: 82 | return logical.ErrorResponse(err.Error()), nil 83 | case errutil.InternalError: 84 | return nil, err 85 | } 86 | } 87 | 88 | csrb, err := parsedBundle.ToCSRBundle() 89 | if err != nil { 90 | return nil, fmt.Errorf("error converting raw CSR bundle to CSR bundle: %w", err) 91 | } 92 | 93 | resp = &logical.Response{ 94 | Data: map[string]interface{}{}, 95 | } 96 | 97 | switch format { 98 | case "pem": 99 | resp.Data["csr"] = csrb.CSR 100 | if exported { 101 | resp.Data["private_key"] = csrb.PrivateKey 102 | resp.Data["private_key_type"] = csrb.PrivateKeyType 103 | } 104 | 105 | case "pem_bundle": 106 | resp.Data["csr"] = csrb.CSR 107 | if exported { 108 | resp.Data["csr"] = fmt.Sprintf("%s\n%s", csrb.PrivateKey, csrb.CSR) 109 | resp.Data["private_key"] = csrb.PrivateKey 110 | resp.Data["private_key_type"] = csrb.PrivateKeyType 111 | } 112 | 113 | case "der": 114 | resp.Data["csr"] = base64.StdEncoding.EncodeToString(parsedBundle.CSRBytes) 115 | if exported { 116 | resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) 117 | resp.Data["private_key_type"] = csrb.PrivateKeyType 118 | } 119 | } 120 | 121 | if data.Get("private_key_format").(string) == "pkcs8" { 122 | err = convertRespToPKCS8(resp) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | cb := &certutil.CertBundle{} 129 | cb.PrivateKey = csrb.PrivateKey 130 | cb.PrivateKeyType = csrb.PrivateKeyType 131 | 132 | entry, err := logical.StorageEntryJSON("config/ca_bundle", cb) 133 | if err != nil { 134 | return nil, err 135 | } 136 | err = req.Storage.Put(ctx, entry) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | return resp, nil 142 | } 143 | 144 | func (b *backend) pathSetSignedIntermediate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 145 | cert := data.Get("certificate").(string) 146 | 147 | if cert == "" { 148 | return logical.ErrorResponse("no certificate provided in the \"certificate\" parameter"), nil 149 | } 150 | 151 | inputBundle, err := certutil.ParsePEMBundle(cert) 152 | if err != nil { 153 | switch err.(type) { 154 | case errutil.InternalError: 155 | return nil, err 156 | default: 157 | return logical.ErrorResponse(err.Error()), nil 158 | } 159 | } 160 | 161 | if inputBundle.Certificate == nil { 162 | return logical.ErrorResponse("supplied certificate could not be successfully parsed"), nil 163 | } 164 | 165 | cb := &certutil.CertBundle{} 166 | entry, err := req.Storage.Get(ctx, "config/ca_bundle") 167 | if err != nil { 168 | return nil, err 169 | } 170 | if entry == nil { 171 | return logical.ErrorResponse("could not find any existing entry with a private key"), nil 172 | } 173 | 174 | err = entry.DecodeJSON(cb) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | if len(cb.PrivateKey) == 0 || cb.PrivateKeyType == "" { 180 | return logical.ErrorResponse("could not find an existing private key"), nil 181 | } 182 | 183 | parsedCB, err := cb.ToParsedCertBundle() 184 | if err != nil { 185 | return nil, err 186 | } 187 | if parsedCB.PrivateKey == nil { 188 | return nil, fmt.Errorf("saved key could not be parsed successfully") 189 | } 190 | 191 | inputBundle.PrivateKey = parsedCB.PrivateKey 192 | inputBundle.PrivateKeyType = parsedCB.PrivateKeyType 193 | inputBundle.PrivateKeyBytes = parsedCB.PrivateKeyBytes 194 | 195 | if !inputBundle.Certificate.IsCA { 196 | return logical.ErrorResponse("the given certificate is not marked for CA use and cannot be used with this backend"), nil 197 | } 198 | 199 | if err := inputBundle.Verify(); err != nil { 200 | return nil, fmt.Errorf("verification of parsed bundle failed: %w", err) 201 | } 202 | 203 | cb, err = inputBundle.ToCertBundle() 204 | if err != nil { 205 | return nil, fmt.Errorf("error converting raw values into cert bundle: %w", err) 206 | } 207 | 208 | entry, err = logical.StorageEntryJSON("config/ca_bundle", cb) 209 | if err != nil { 210 | return nil, err 211 | } 212 | err = req.Storage.Put(ctx, entry) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | entry.Key = "certs/" + normalizeSerial(cb.SerialNumber) 218 | entry.Value = inputBundle.CertificateBytes 219 | err = req.Storage.Put(ctx, entry) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | // For ease of later use, also store just the certificate at a known 225 | // location 226 | entry.Key = "ca" 227 | entry.Value = inputBundle.CertificateBytes 228 | err = req.Storage.Put(ctx, entry) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | // Build a fresh CRL 234 | err = buildCRL(ctx, b, req, true) 235 | 236 | return nil, err 237 | } 238 | 239 | const pathGenerateIntermediateHelpSyn = ` 240 | Generate a new CSR and private key used for signing. 241 | ` 242 | 243 | const pathGenerateIntermediateHelpDesc = ` 244 | See the API documentation for more information. 245 | ` 246 | 247 | const pathSetSignedIntermediateHelpSyn = ` 248 | Provide the signed intermediate CA cert. 249 | ` 250 | 251 | const pathSetSignedIntermediateHelpDesc = ` 252 | See the API documentation for more information. 253 | ` 254 | -------------------------------------------------------------------------------- /plugin/pki/path_issue_sign.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/hashicorp/vault/sdk/framework" 11 | "github.com/hashicorp/vault/sdk/helper/certutil" 12 | "github.com/hashicorp/vault/sdk/helper/consts" 13 | "github.com/hashicorp/vault/sdk/helper/errutil" 14 | "github.com/hashicorp/vault/sdk/logical" 15 | ) 16 | 17 | func pathIssue(b *backend) *framework.Path { 18 | ret := &framework.Path{ 19 | Pattern: "issue/" + framework.GenericNameRegex("role"), 20 | 21 | Callbacks: map[logical.Operation]framework.OperationFunc{ 22 | logical.UpdateOperation: b.pathIssue, 23 | }, 24 | 25 | HelpSynopsis: pathIssueHelpSyn, 26 | HelpDescription: pathIssueHelpDesc, 27 | } 28 | 29 | ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) 30 | return ret 31 | } 32 | 33 | func pathSign(b *backend) *framework.Path { 34 | ret := &framework.Path{ 35 | Pattern: "sign/" + framework.GenericNameRegex("role"), 36 | 37 | Callbacks: map[logical.Operation]framework.OperationFunc{ 38 | logical.UpdateOperation: b.pathSign, 39 | }, 40 | 41 | HelpSynopsis: pathSignHelpSyn, 42 | HelpDescription: pathSignHelpDesc, 43 | } 44 | 45 | ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) 46 | 47 | ret.Fields["csr"] = &framework.FieldSchema{ 48 | Type: framework.TypeString, 49 | Default: "", 50 | Description: `PEM-format CSR to be signed.`, 51 | } 52 | 53 | return ret 54 | } 55 | 56 | func pathSignVerbatim(b *backend) *framework.Path { 57 | ret := &framework.Path{ 58 | Pattern: "sign-verbatim" + framework.OptionalParamRegex("role"), 59 | 60 | Callbacks: map[logical.Operation]framework.OperationFunc{ 61 | logical.UpdateOperation: b.pathSignVerbatim, 62 | }, 63 | 64 | HelpSynopsis: pathSignHelpSyn, 65 | HelpDescription: pathSignHelpDesc, 66 | } 67 | 68 | ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) 69 | 70 | ret.Fields["csr"] = &framework.FieldSchema{ 71 | Type: framework.TypeString, 72 | Default: "", 73 | Description: `PEM-format CSR to be signed. Values will be 74 | taken verbatim from the CSR, except for 75 | basic constraints.`, 76 | } 77 | 78 | ret.Fields["key_usage"] = &framework.FieldSchema{ 79 | Type: framework.TypeCommaStringSlice, 80 | Default: []string{"DigitalSignature", "KeyAgreement", "KeyEncipherment"}, 81 | Description: `A comma-separated string or list of key usages (not extended 82 | key usages). Valid values can be found at 83 | https://golang.org/pkg/crypto/x509/#KeyUsage 84 | -- simply drop the "KeyUsage" part of the name. 85 | To remove all key usages from being set, set 86 | this value to an empty list.`, 87 | } 88 | 89 | ret.Fields["ext_key_usage"] = &framework.FieldSchema{ 90 | Type: framework.TypeCommaStringSlice, 91 | Default: []string{}, 92 | Description: `A comma-separated string or list of extended key usages. Valid values can be found at 93 | https://golang.org/pkg/crypto/x509/#ExtKeyUsage 94 | -- simply drop the "ExtKeyUsage" part of the name. 95 | To remove all key usages from being set, set 96 | this value to an empty list.`, 97 | } 98 | 99 | ret.Fields["ext_key_usage_oids"] = &framework.FieldSchema{ 100 | Type: framework.TypeCommaStringSlice, 101 | Description: `A comma-separated string or list of extended key usage oids.`, 102 | } 103 | 104 | return ret 105 | } 106 | 107 | // pathIssue issues a certificate and private key from given parameters, 108 | // subject to role restrictions 109 | func (b *backend) pathIssue(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 110 | roleName := data.Get("role").(string) 111 | 112 | // Get the role 113 | role, err := b.getRole(ctx, req.Storage, roleName) 114 | if err != nil { 115 | return nil, err 116 | } 117 | if role == nil { 118 | return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", roleName)), nil 119 | } 120 | 121 | if role.KeyType == "any" { 122 | return logical.ErrorResponse("role key type \"any\" not allowed for issuing certificates, only signing"), nil 123 | } 124 | 125 | return b.pathIssueSignCert(ctx, req, data, role, false, false) 126 | } 127 | 128 | // pathSign issues a certificate from a submitted CSR, subject to role 129 | // restrictions 130 | func (b *backend) pathSign(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 131 | roleName := data.Get("role").(string) 132 | 133 | // Get the role 134 | role, err := b.getRole(ctx, req.Storage, roleName) 135 | if err != nil { 136 | return nil, err 137 | } 138 | if role == nil { 139 | return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", roleName)), nil 140 | } 141 | 142 | return b.pathIssueSignCert(ctx, req, data, role, true, false) 143 | } 144 | 145 | // pathSignVerbatim issues a certificate from a submitted CSR, *not* subject to 146 | // role restrictions 147 | func (b *backend) pathSignVerbatim(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 148 | roleName := data.Get("role").(string) 149 | 150 | // Get the role if one was specified 151 | role, err := b.getRole(ctx, req.Storage, roleName) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | entry := &roleEntry{ 157 | AllowLocalhost: true, 158 | AllowAnyName: true, 159 | AllowIPSANs: true, 160 | EnforceHostnames: false, 161 | KeyType: "any", 162 | UseCSRCommonName: true, 163 | UseCSRSANs: true, 164 | AllowedURISANs: []string{"*"}, 165 | AllowedSerialNumbers: []string{"*"}, 166 | GenerateLease: new(bool), 167 | KeyUsage: data.Get("key_usage").([]string), 168 | ExtKeyUsage: data.Get("ext_key_usage").([]string), 169 | ExtKeyUsageOIDs: data.Get("ext_key_usage_oids").([]string), 170 | } 171 | 172 | *entry.GenerateLease = false 173 | 174 | if role != nil { 175 | if role.TTL > 0 { 176 | entry.TTL = role.TTL 177 | } 178 | if role.MaxTTL > 0 { 179 | entry.MaxTTL = role.MaxTTL 180 | } 181 | if role.GenerateLease != nil { 182 | *entry.GenerateLease = *role.GenerateLease 183 | } 184 | entry.NoStore = role.NoStore 185 | } 186 | 187 | return b.pathIssueSignCert(ctx, req, data, entry, true, true) 188 | } 189 | 190 | func (b *backend) pathIssueSignCert(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry, useCSR, useCSRValues bool) (*logical.Response, error) { 191 | // If storing the certificate and on a performance standby or secondary, forward this request on to the primary 192 | if !role.NoStore && b.System().ReplicationState(). 193 | HasState(consts.ReplicationPerformanceStandby|consts.ReplicationPerformanceSecondary) { 194 | return nil, logical.ErrReadOnly 195 | } 196 | 197 | format := getFormat(data) 198 | if format == "" { 199 | return logical.ErrorResponse( 200 | `the "format" path parameter must be "pem", "der", or "pem_bundle"`), nil 201 | } 202 | 203 | var caErr error 204 | signingBundle, caErr := fetchCAInfo(ctx, req) 205 | switch caErr.(type) { 206 | case errutil.UserError: 207 | return nil, errutil.UserError{Err: fmt.Sprintf( 208 | "could not fetch the CA certificate (was one set?): %s", caErr)} 209 | case errutil.InternalError: 210 | return nil, errutil.InternalError{Err: fmt.Sprintf( 211 | "error fetching CA certificate: %s", caErr)} 212 | } 213 | 214 | input := &dataBundle{ 215 | req: req, 216 | apiData: data, 217 | role: role, 218 | signingBundle: signingBundle, 219 | } 220 | var parsedBundle *certutil.ParsedCertBundle 221 | var err error 222 | if useCSR { 223 | parsedBundle, err = signCert(b, input, false, useCSRValues) 224 | } else { 225 | parsedBundle, err = generateCert(ctx, b, input, false) 226 | } 227 | if err != nil { 228 | switch err.(type) { 229 | case errutil.UserError: 230 | return logical.ErrorResponse(err.Error()), nil 231 | case errutil.InternalError: 232 | return nil, err 233 | } 234 | } 235 | 236 | signingCB, err := signingBundle.ToCertBundle() 237 | if err != nil { 238 | return nil, fmt.Errorf("error converting raw signing bundle to cert bundle: %w", err) 239 | } 240 | 241 | cb, err := parsedBundle.ToCertBundle() 242 | if err != nil { 243 | return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err) 244 | } 245 | 246 | respData := map[string]interface{}{ 247 | "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), 248 | "serial_number": cb.SerialNumber, 249 | } 250 | 251 | switch format { 252 | case "pem": 253 | respData["issuing_ca"] = signingCB.Certificate 254 | respData["certificate"] = cb.Certificate 255 | if cb.CAChain != nil && len(cb.CAChain) > 0 { 256 | respData["ca_chain"] = cb.CAChain 257 | } 258 | if !useCSR { 259 | respData["private_key"] = cb.PrivateKey 260 | respData["private_key_type"] = cb.PrivateKeyType 261 | } 262 | 263 | case "pem_bundle": 264 | respData["issuing_ca"] = signingCB.Certificate 265 | respData["certificate"] = cb.ToPEMBundle() 266 | if cb.CAChain != nil && len(cb.CAChain) > 0 { 267 | respData["ca_chain"] = cb.CAChain 268 | } 269 | if !useCSR { 270 | respData["private_key"] = cb.PrivateKey 271 | respData["private_key_type"] = cb.PrivateKeyType 272 | } 273 | 274 | case "der": 275 | respData["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) 276 | respData["issuing_ca"] = base64.StdEncoding.EncodeToString(signingBundle.CertificateBytes) 277 | 278 | var caChain []string 279 | for _, caCert := range parsedBundle.CAChain { 280 | caChain = append(caChain, base64.StdEncoding.EncodeToString(caCert.Bytes)) 281 | } 282 | if len(caChain) > 0 { 283 | respData["ca_chain"] = caChain 284 | } 285 | 286 | if !useCSR { 287 | respData["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) 288 | respData["private_key_type"] = cb.PrivateKeyType 289 | } 290 | } 291 | 292 | var resp *logical.Response 293 | switch { 294 | case role.GenerateLease == nil: 295 | return nil, fmt.Errorf("generate lease in role is nil") 296 | case !*role.GenerateLease: 297 | // If lease generation is disabled do not populate `Secret` field in 298 | // the response 299 | resp = &logical.Response{ 300 | Data: respData, 301 | } 302 | default: 303 | resp = b.Secret(SecretCertsType).Response( 304 | respData, 305 | map[string]interface{}{ 306 | "serial_number": cb.SerialNumber, 307 | }) 308 | resp.Secret.TTL = time.Until(parsedBundle.Certificate.NotAfter) 309 | } 310 | 311 | if data.Get("private_key_format").(string) == "pkcs8" { 312 | err = convertRespToPKCS8(resp) 313 | if err != nil { 314 | return nil, err 315 | } 316 | } 317 | 318 | if !role.NoStore { 319 | err = req.Storage.Put(ctx, &logical.StorageEntry{ 320 | Key: "certs/" + normalizeSerial(cb.SerialNumber), 321 | Value: parsedBundle.CertificateBytes, 322 | }) 323 | if err != nil { 324 | return nil, fmt.Errorf("unable to store certificate locally: %w", err) 325 | } 326 | } 327 | 328 | if useCSR { 329 | if role.UseCSRCommonName && data.Get("common_name").(string) != "" { 330 | resp.AddWarning("the common_name field was provided but the role is set with \"use_csr_common_name\" set to true") 331 | } 332 | if role.UseCSRSANs && data.Get("alt_names").(string) != "" { 333 | resp.AddWarning("the alt_names field was provided but the role is set with \"use_csr_sans\" set to true") 334 | } 335 | } 336 | 337 | policyMap, err := getPolicyRoleMap(ctx, req.Storage) 338 | if err != nil { 339 | return nil, fmt.Errorf("unable to get policy map: %w", err) 340 | } 341 | if policyMap.Roles[data.Get("role").(string)].ImportPolicy != "" { 342 | sn := normalizeSerial(cb.SerialNumber) 343 | log.Printf("%s Puting certificate with serial number %s to the Venafi import queue\n", logPrefixVenafiImport, sn) 344 | 345 | err = req.Storage.Put(ctx, &logical.StorageEntry{ 346 | Key: "import-queue/" + data.Get("role").(string) + "/" + sn, 347 | Value: parsedBundle.CertificateBytes, 348 | }) 349 | if err != nil { 350 | log.Printf("Unable to store certificate in import queue: %s", err) 351 | } 352 | } 353 | 354 | log.Printf("Returning sign response") 355 | return resp, nil 356 | } 357 | 358 | const pathIssueHelpSyn = ` 359 | Request a certificate using a certain role with the provided details. 360 | ` 361 | 362 | const pathIssueHelpDesc = ` 363 | This path allows requesting a certificate to be issued according to the 364 | policy of the given role. The certificate will only be issued if the 365 | requested details are allowed by the role policy. 366 | 367 | This path returns a certificate and a private key. If you want a workflow 368 | that does not expose a private key, generate a CSR locally and use the 369 | sign path instead. 370 | ` 371 | 372 | const pathSignHelpSyn = ` 373 | Request certificates using a certain role with the provided details. 374 | ` 375 | 376 | const pathSignHelpDesc = ` 377 | This path allows requesting certificates to be issued according to the 378 | policy of the given role. The certificate will only be issued if the 379 | requested common name is allowed by the role policy. 380 | 381 | This path requires a CSR; if you want Vault to generate a private key 382 | for you, use the issue path instead. 383 | ` 384 | -------------------------------------------------------------------------------- /plugin/pki/path_revoke.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/helper/errutil" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | func pathRevoke(b *backend) *framework.Path { 14 | return &framework.Path{ 15 | Pattern: `revoke`, 16 | Fields: map[string]*framework.FieldSchema{ 17 | "serial_number": &framework.FieldSchema{ 18 | Type: framework.TypeString, 19 | Description: `Certificate serial number, in colon- or 20 | hyphen-separated octal`, 21 | }, 22 | }, 23 | 24 | Callbacks: map[logical.Operation]framework.OperationFunc{ 25 | logical.UpdateOperation: b.pathRevokeWrite, 26 | }, 27 | 28 | HelpSynopsis: pathRevokeHelpSyn, 29 | HelpDescription: pathRevokeHelpDesc, 30 | } 31 | } 32 | 33 | func pathRotateCRL(b *backend) *framework.Path { 34 | return &framework.Path{ 35 | Pattern: `crl/rotate`, 36 | 37 | Callbacks: map[logical.Operation]framework.OperationFunc{ 38 | logical.ReadOperation: b.pathRotateCRLRead, 39 | }, 40 | 41 | HelpSynopsis: pathRotateCRLHelpSyn, 42 | HelpDescription: pathRotateCRLHelpDesc, 43 | } 44 | } 45 | 46 | func (b *backend) pathRevokeWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 47 | serial := data.Get("serial_number").(string) 48 | if len(serial) == 0 { 49 | return logical.ErrorResponse("The serial number must be provided"), nil 50 | } 51 | 52 | // We store and identify by lowercase colon-separated hex, but other 53 | // utilities use dashes and/or uppercase, so normalize 54 | serial = strings.Replace(strings.ToLower(serial), "-", ":", -1) 55 | 56 | b.revokeStorageLock.Lock() 57 | defer b.revokeStorageLock.Unlock() 58 | 59 | return revokeCert(ctx, b, req, serial, false) 60 | } 61 | 62 | func (b *backend) pathRotateCRLRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 63 | b.revokeStorageLock.RLock() 64 | defer b.revokeStorageLock.RUnlock() 65 | 66 | crlErr := buildCRL(ctx, b, req, false) 67 | switch crlErr.(type) { 68 | case errutil.UserError: 69 | return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil 70 | case errutil.InternalError: 71 | return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr) 72 | default: 73 | return &logical.Response{ 74 | Data: map[string]interface{}{ 75 | "success": true, 76 | }, 77 | }, nil 78 | } 79 | } 80 | 81 | const pathRevokeHelpSyn = ` 82 | Revoke a certificate by serial number. 83 | ` 84 | 85 | const pathRevokeHelpDesc = ` 86 | This allows certificates to be revoked using its serial number. A root token is required. 87 | ` 88 | 89 | const pathRotateCRLHelpSyn = ` 90 | Force a rebuild of the CRL. 91 | ` 92 | 93 | const pathRotateCRLHelpDesc = ` 94 | Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required. 95 | ` 96 | -------------------------------------------------------------------------------- /plugin/pki/path_tidy.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "fmt" 7 | "net/http" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/sdk/framework" 12 | "github.com/hashicorp/vault/sdk/helper/consts" 13 | "github.com/hashicorp/vault/sdk/logical" 14 | ) 15 | 16 | func pathTidy(b *backend) *framework.Path { 17 | return &framework.Path{ 18 | Pattern: "tidy", 19 | Fields: map[string]*framework.FieldSchema{ 20 | "tidy_cert_store": &framework.FieldSchema{ 21 | Type: framework.TypeBool, 22 | Description: `Set to true to enable tidying up 23 | the certificate store`, 24 | }, 25 | 26 | "tidy_revocation_list": &framework.FieldSchema{ 27 | Type: framework.TypeBool, 28 | Description: `Deprecated; synonym for 'tidy_revoked_certs`, 29 | }, 30 | 31 | "tidy_revoked_certs": &framework.FieldSchema{ 32 | Type: framework.TypeBool, 33 | Description: `Set to true to expire all revoked 34 | and expired certificates, removing them both from the CRL and from storage. The 35 | CRL will be rotated if this causes any values to be removed.`, 36 | }, 37 | 38 | "safety_buffer": &framework.FieldSchema{ 39 | Type: framework.TypeDurationSecond, 40 | Description: `The amount of extra time that must have passed 41 | beyond certificate expiration before it is removed 42 | from the backend storage and/or revocation list. 43 | Defaults to 72 hours.`, 44 | Default: 259200, //72h, but TypeDurationSecond currently requires defaults to be int 45 | }, 46 | }, 47 | 48 | Callbacks: map[logical.Operation]framework.OperationFunc{ 49 | logical.UpdateOperation: b.pathTidyWrite, 50 | }, 51 | 52 | HelpSynopsis: pathTidyHelpSyn, 53 | HelpDescription: pathTidyHelpDesc, 54 | } 55 | } 56 | 57 | func (b *backend) pathTidyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 58 | // If we are a performance standby forward the request to the active node 59 | if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { 60 | return nil, logical.ErrReadOnly 61 | } 62 | 63 | safetyBuffer := d.Get("safety_buffer").(int) 64 | tidyCertStore := d.Get("tidy_cert_store").(bool) 65 | tidyRevokedCerts := d.Get("tidy_revoked_certs").(bool) 66 | tidyRevocationList := d.Get("tidy_revocation_list").(bool) 67 | 68 | if safetyBuffer < 1 { 69 | return logical.ErrorResponse("safety_buffer must be greater than zero"), nil 70 | } 71 | 72 | bufferDuration := time.Duration(safetyBuffer) * time.Second 73 | 74 | if !atomic.CompareAndSwapUint32(b.tidyCASGuard, 0, 1) { 75 | resp := &logical.Response{} 76 | resp.AddWarning("Tidy operation already in progress.") 77 | return resp, nil 78 | } 79 | 80 | // Tests using framework will screw up the storage so make a locally 81 | // scoped req to hold a reference 82 | req = &logical.Request{ 83 | Storage: req.Storage, 84 | } 85 | 86 | go func() { 87 | defer atomic.StoreUint32(b.tidyCASGuard, 0) 88 | 89 | // Don't cancel when the original client request goes away 90 | ctx = context.Background() 91 | 92 | logger := b.Logger().Named("tidy") 93 | 94 | doTidy := func() error { 95 | if tidyCertStore { 96 | serials, err := req.Storage.List(ctx, "certs/") 97 | if err != nil { 98 | return fmt.Errorf("error fetching list of certs: %w", err) 99 | } 100 | 101 | for _, serial := range serials { 102 | certEntry, err := req.Storage.Get(ctx, "certs/"+serial) 103 | if err != nil { 104 | return fmt.Errorf("error fetching certificate %q: %w", serial, err) 105 | } 106 | 107 | if certEntry == nil { 108 | logger.Warn("certificate entry is nil; tidying up since it is no longer useful for any server operations", "serial", serial) 109 | if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil { 110 | return fmt.Errorf("error deleting nil entry with serial %s: %w", serial, err) 111 | } 112 | continue 113 | } 114 | 115 | if certEntry.Value == nil || len(certEntry.Value) == 0 { 116 | logger.Warn("certificate entry has no value; tidying up since it is no longer useful for any server operations", "serial", serial) 117 | if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil { 118 | return fmt.Errorf("error deleting entry with nil value with serial %s: %w", serial, err) 119 | } 120 | } 121 | 122 | cert, err := x509.ParseCertificate(certEntry.Value) 123 | if err != nil { 124 | return fmt.Errorf("unable to parse stored certificate with serial %q: %w", serial, err) 125 | } 126 | 127 | if time.Now().After(cert.NotAfter.Add(bufferDuration)) { 128 | if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil { 129 | return fmt.Errorf("error deleting serial %q from storage: %w", serial, err) 130 | } 131 | } 132 | } 133 | } 134 | 135 | if tidyRevokedCerts || tidyRevocationList { 136 | b.revokeStorageLock.Lock() 137 | defer b.revokeStorageLock.Unlock() 138 | 139 | tidiedRevoked := false 140 | 141 | revokedSerials, err := req.Storage.List(ctx, "revoked/") 142 | if err != nil { 143 | return fmt.Errorf("error fetching list of revoked certs: %w", err) 144 | } 145 | 146 | var revInfo revocationInfo 147 | for _, serial := range revokedSerials { 148 | revokedEntry, err := req.Storage.Get(ctx, "revoked/"+serial) 149 | if err != nil { 150 | return fmt.Errorf("unable to fetch revoked cert with serial %q: %w", serial, err) 151 | } 152 | 153 | if revokedEntry == nil { 154 | logger.Warn("revoked entry is nil; tidying up since it is no longer useful for any server operations", "serial", serial) 155 | if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil { 156 | return fmt.Errorf("error deleting nil revoked entry with serial %s: %w", serial, err) 157 | } 158 | } 159 | 160 | if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 { 161 | logger.Warn("revoked entry has nil value; tidying up since it is no longer useful for any server operations", "serial", serial) 162 | if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil { 163 | return fmt.Errorf("error deleting revoked entry with nil value with serial %s: %w", serial, err) 164 | } 165 | } 166 | 167 | err = revokedEntry.DecodeJSON(&revInfo) 168 | if err != nil { 169 | return fmt.Errorf("error decoding revocation entry for serial %q: %w", serial, err) 170 | } 171 | 172 | revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes) 173 | if err != nil { 174 | return fmt.Errorf("unable to parse stored revoked certificate with serial %q: %w", serial, err) 175 | } 176 | 177 | if time.Now().After(revokedCert.NotAfter.Add(bufferDuration)) { 178 | if err := req.Storage.Delete(ctx, "revoked/"+serial); err != nil { 179 | return fmt.Errorf("error deleting serial %q from revoked list: %w", serial, err) 180 | } 181 | if err := req.Storage.Delete(ctx, "certs/"+serial); err != nil { 182 | return fmt.Errorf("error deleting serial %q from store when tidying revoked: %w", serial, err) 183 | } 184 | tidiedRevoked = true 185 | } 186 | } 187 | 188 | if tidiedRevoked { 189 | if err := buildCRL(ctx, b, req, false); err != nil { 190 | return err 191 | } 192 | } 193 | } 194 | 195 | return nil 196 | } 197 | 198 | if err := doTidy(); err != nil { 199 | logger.Error("error running tidy", "error", err) 200 | return 201 | } 202 | }() 203 | 204 | resp := &logical.Response{} 205 | resp.AddWarning("Tidy operation successfully started. Any information from the operation will be printed to Vault's server logs.") 206 | return logical.RespondWithStatusCode(resp, req, http.StatusAccepted) 207 | } 208 | 209 | const pathTidyHelpSyn = ` 210 | Tidy up the backend by removing expired certificates, revocation information, 211 | or both. 212 | ` 213 | 214 | const pathTidyHelpDesc = ` 215 | This endpoint allows expired certificates and/or revocation information to be 216 | removed from the backend, freeing up storage and shortening CRLs. 217 | 218 | For safety, this function is a noop if called without parameters; cleanup from 219 | normal certificate storage must be enabled with 'tidy_cert_store' and cleanup 220 | from revocation information must be enabled with 'tidy_revocation_list'. 221 | 222 | The 'safety_buffer' parameter is useful to ensure that clock skew amongst your 223 | hosts cannot lead to a certificate being removed from the CRL while it is still 224 | considered valid by other hosts (for instance, if their clocks are a few 225 | minutes behind). The 'safety_buffer' parameter can be an integer number of 226 | seconds or a string duration like "72h". 227 | 228 | All certificates and/or revocation information currently stored in the backend 229 | will be checked when this endpoint is hit. The expiration of the 230 | certificate/revocation information of each certificate being held in 231 | certificate storage or in revocation information will then be checked. If the 232 | current time, minus the value of 'safety_buffer', is greater than the 233 | expiration, it will be removed. 234 | ` 235 | -------------------------------------------------------------------------------- /plugin/pki/path_venafi_policy_sync.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/Venafi/vcert/v4/pkg/endpoint" 7 | "github.com/hashicorp/vault/sdk/framework" 8 | hconsts "github.com/hashicorp/vault/sdk/helper/consts" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "log" 11 | "regexp" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const venafiSyncPolicyListPath = "venafi-sync-policies" 17 | 18 | func pathVenafiPolicySync(b *backend) *framework.Path { 19 | ret := &framework.Path{ 20 | Pattern: venafiSyncPolicyListPath, 21 | 22 | Callbacks: map[logical.Operation]framework.OperationFunc{ 23 | logical.ReadOperation: b.pathReadVenafiPolicySync, 24 | }, 25 | } 26 | ret.Fields = addNonCACommonFields(map[string]*framework.FieldSchema{}) 27 | return ret 28 | } 29 | 30 | func (b *backend) pathReadVenafiPolicySync(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { 31 | //Get role list with role sync param 32 | log.Println("starting to read sync roles") 33 | roles, err := req.Storage.List(ctx, "role/") 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if len(roles) == 0 { 39 | return nil, fmt.Errorf("No roles found in storage") 40 | } 41 | 42 | var entries []string 43 | 44 | for _, roleName := range roles { 45 | log.Println("looking role ", roleName) 46 | // Read previous role parameters 47 | pkiRoleEntry, err := b.getPKIRoleEntry(ctx, req.Storage, roleName) 48 | if err != nil { 49 | log.Printf("%s", err) 50 | continue 51 | } 52 | 53 | if pkiRoleEntry == nil { 54 | continue 55 | } 56 | 57 | policyMap, err := getPolicyRoleMap(ctx, req.Storage) 58 | if err != nil { 59 | return 60 | } 61 | 62 | //Get Venafi policy in entry format 63 | if policyMap.Roles[roleName].DefaultsPolicy == "" { 64 | continue 65 | } 66 | 67 | var entry []string 68 | entry = append(entry, fmt.Sprintf("role: %s sync policy: %s", roleName, policyMap.Roles[roleName].DefaultsPolicy)) 69 | entries = append(entries, entry...) 70 | 71 | } 72 | return logical.ListResponse(entries), nil 73 | } 74 | 75 | func (b *backend) syncRoleWithVenafiPolicyRegister(conf *logical.BackendConfig) { 76 | log.Printf("%s registering policy sync controller", logPrefixVenafiPolicyEnforcement) 77 | b.taskStorage.register("policy-sync-controller", func() { 78 | err := b.syncPolicyEnforcementAndRoleDefaults(conf) 79 | if err != nil { 80 | log.Printf("%s %s", logPrefixVenafiPolicyEnforcement, err) 81 | } 82 | }, 1, time.Second*15) 83 | } 84 | 85 | func (b *backend) syncPolicyEnforcementAndRoleDefaults(conf *logical.BackendConfig) (err error) { 86 | replicationState := conf.System.ReplicationState() 87 | //Checking if we are on master or on the stanby Vault server 88 | isSlave := !(conf.System.LocalMount() || !replicationState.HasState(hconsts.ReplicationPerformanceSecondary)) || 89 | replicationState.HasState(hconsts.ReplicationDRSecondary) || 90 | replicationState.HasState(hconsts.ReplicationPerformanceStandby) 91 | if isSlave { 92 | log.Printf("%s We're on slave. Sleeping", logPrefixVenafiPolicyEnforcement) 93 | return 94 | } 95 | log.Printf("%s We're on master. Starting to synchronise policy", logPrefixVenafiPolicyEnforcement) 96 | 97 | ctx := context.Background() 98 | //Get policy list for enforcement sync 99 | policiesRaw, err := b.storage.List(ctx, venafiPolicyPath) 100 | if err != nil { 101 | return err 102 | } 103 | var policies []string 104 | 105 | //Removing from policy list repeated policy name with / at the end 106 | for _, p := range policiesRaw { 107 | if !strings.Contains(p, "/") { 108 | policies = append(policies, p) 109 | } 110 | } 111 | 112 | for _, policyName := range policies { 113 | 114 | policyConfig, err := b.getVenafiPolicyConfig(ctx, &b.storage, policyName) 115 | if err != nil { 116 | log.Printf("%s Error getting policy config for policy %s: %s", logPrefixVenafiPolicyEnforcement, policyName, err) 117 | continue 118 | } 119 | 120 | if policyConfig == nil { 121 | log.Printf("%s Policy config for %s is nil. Skipping", logPrefixVenafiPolicyEnforcement, policyName) 122 | continue 123 | } 124 | 125 | log.Printf("%s check last policy updated time", logPrefixVenafiPolicyEnforcement) 126 | timePassed := time.Now().Unix() - policyConfig.LastPolicyUpdateTime 127 | 128 | //update only if needed 129 | //TODO: Make test to check this refresh 130 | if (timePassed) < policyConfig.AutoRefreshInterval { 131 | continue 132 | } 133 | 134 | //Refresh Venafi policy regexes 135 | err = b.refreshVenafiPolicyEnforcementContent(b.storage, policyName) 136 | if err != nil { 137 | log.Printf("%s Error refreshing venafi policy content: %s", logPrefixVenafiPolicyEnforcement, err) 138 | continue 139 | } 140 | //Refresh roles defaults 141 | //Get role list with role sync param 142 | rolesList, err := b.getRolesListForVenafiPolicy(ctx, b.storage, policyName) 143 | if err != nil { 144 | continue 145 | } 146 | 147 | if len(rolesList.defaultsRoles) == 0 { 148 | log.Printf("%s No roles found for refreshing defaults in policy %s", logPrefixVenafiRoleyDefaults, policyName) 149 | continue 150 | } 151 | 152 | for _, roleName := range rolesList.defaultsRoles { 153 | log.Printf("Synchronizing role %s", roleName) 154 | msg := b.synchronizeRoleDefaults(ctx, b.storage, roleName, policyName) 155 | log.Printf("%s %s", logPrefixVenafiRoleyDefaults, msg) 156 | } 157 | 158 | //policy config's credentials may be got updated so get it from storage again before saving it. 159 | policyConfig, _ = b.getVenafiPolicyConfig(ctx, &b.storage, policyName) 160 | 161 | //set new last updated 162 | policyConfig.LastPolicyUpdateTime = time.Now().Unix() 163 | 164 | //put new policy entry with updated time value 165 | jsonEntry, err := logical.StorageEntryJSON(venafiPolicyPath+policyName, policyConfig) 166 | if err != nil { 167 | return fmt.Errorf("Error converting policy config into JSON: %s", err) 168 | 169 | } 170 | if err := b.storage.Put(ctx, jsonEntry); err != nil { 171 | return fmt.Errorf("Error saving policy last update time: %s", err) 172 | 173 | } 174 | } 175 | 176 | return err 177 | } 178 | 179 | func (b *backend) synchronizeRoleDefaults(ctx context.Context, storage logical.Storage, roleName string, policyName string) (msg string) { 180 | // Read previous role parameters 181 | pkiRoleEntry, err := b.getPKIRoleEntry(ctx, storage, roleName) 182 | if err != nil { 183 | return fmt.Sprintf("%s", err) 184 | } 185 | 186 | if pkiRoleEntry == nil { 187 | return fmt.Sprintf("PKI role %s is empty or does not exist", roleName) 188 | } 189 | 190 | //Get Venafi policy in entry format 191 | policyMap, err := getPolicyRoleMap(ctx, storage) 192 | if err != nil { 193 | return 194 | } 195 | if policyMap.Roles[roleName].DefaultsPolicy == "" { 196 | return fmt.Sprintf("role %s do not have venafi_defaults_policy attribute", roleName) 197 | } 198 | 199 | entry, err := storage.Get(ctx, venafiPolicyPath+policyName) 200 | if err != nil { 201 | return fmt.Sprintf("%s", err) 202 | } 203 | 204 | if entry == nil { 205 | return "entry is nil" 206 | } 207 | 208 | var policy venafiPolicyConfigEntry 209 | err = entry.DecodeJSON(&policy) 210 | if err != nil { 211 | return fmt.Sprintf("%s", err) 212 | } 213 | 214 | secret, err := b.getVenafiSecret(ctx, &storage, policy.VenafiSecret) 215 | if err != nil { 216 | return fmt.Sprintf("%s", err) 217 | } 218 | 219 | zone := policy.Zone 220 | if zone == "" { 221 | zone = secret.Zone 222 | } 223 | 224 | venafiPolicyEntry, err := b.getVenafiPolicyParams(ctx, storage, policyName, zone) 225 | if err != nil { 226 | return fmt.Sprintf("%s", err) 227 | } 228 | 229 | // Replace PKI entry with Venafi policy values 230 | replacePKIValue(&pkiRoleEntry.OU, venafiPolicyEntry.OU) 231 | replacePKIValue(&pkiRoleEntry.Organization, venafiPolicyEntry.Organization) 232 | replacePKIValue(&pkiRoleEntry.Country, venafiPolicyEntry.Country) 233 | replacePKIValue(&pkiRoleEntry.Locality, venafiPolicyEntry.Locality) 234 | replacePKIValue(&pkiRoleEntry.Province, venafiPolicyEntry.Province) 235 | replacePKIValue(&pkiRoleEntry.StreetAddress, venafiPolicyEntry.StreetAddress) 236 | replacePKIValue(&pkiRoleEntry.PostalCode, venafiPolicyEntry.PostalCode) 237 | 238 | //does not have to configure the role to limit domains 239 | // because the Venafi policy already constrains that area 240 | pkiRoleEntry.AllowAnyName = true 241 | pkiRoleEntry.AllowedDomains = []string{} 242 | pkiRoleEntry.AllowSubdomains = true 243 | //TODO: we need to sync key settings as well. But before it we need to add key type to zone configuration 244 | //in vcert SDK 245 | 246 | // Put new entry 247 | jsonEntry, err := logical.StorageEntryJSON("role/"+roleName, pkiRoleEntry) 248 | if err != nil { 249 | return fmt.Sprintf("Error creating json entry for storage: %s", err) 250 | } 251 | if err := storage.Put(ctx, jsonEntry); err != nil { 252 | return fmt.Sprintf("Error putting entry to storage: %s", err) 253 | } 254 | 255 | return fmt.Sprintf("finished synchronizing role %s", roleName) 256 | } 257 | 258 | func replacePKIValue(original *[]string, zone []string) { 259 | if len(zone) > 0 { 260 | if zone[0] != "" { 261 | *original = zone 262 | } 263 | 264 | } 265 | } 266 | 267 | func (b *backend) getVenafiPolicyParams(ctx context.Context, storage logical.Storage, policyConfig string, syncZone string) (entry roleEntry, err error) { 268 | //Get role params from TPP\Cloud 269 | cl, err := b.ClientVenafi(ctx, &storage, policyConfig) 270 | if err != nil { 271 | return entry, fmt.Errorf("could not create venafi client: %s", err) 272 | } 273 | 274 | cl.SetZone(syncZone) 275 | zone, err := cl.ReadZoneConfiguration() 276 | if (err != nil) && (cl.GetType() == endpoint.ConnectorTypeTPP) { 277 | msg := err.Error() 278 | 279 | //catch the scenario when token is expired and deleted. 280 | var regex = regexp.MustCompile("(expired|invalid)_token") 281 | 282 | //validate if the error is related to a expired accces token, at this moment the only way can validate this is using the error message 283 | //and verify if that message describes errors related to expired access token. 284 | code := getStatusCode(msg) 285 | if code == HTTP_UNAUTHORIZED && regex.MatchString(msg){ 286 | cfg, err := b.getConfig(ctx, &storage, policyConfig) 287 | 288 | if err != nil { 289 | return entry, err 290 | } 291 | 292 | if cfg.Credentials.RefreshToken != "" { 293 | err = synchronizedUpdateAccessToken(cfg, b, ctx, &storage, policyConfig) 294 | 295 | if err != nil { 296 | return entry, err 297 | } 298 | 299 | //everything went fine so get the new client with the new refreshed access token 300 | cl, err := b.ClientVenafi(ctx, &storage, policyConfig) 301 | if err != nil { 302 | return entry, err 303 | } 304 | 305 | b.Logger().Debug("Reading policy configuration again") 306 | 307 | zone, err = cl.ReadZoneConfiguration() 308 | if err != nil { 309 | return entry, err 310 | } else { 311 | entry = roleEntry{ 312 | OU: zone.OrganizationalUnit, 313 | Organization: []string{zone.Organization}, 314 | Country: []string{zone.Country}, 315 | Locality: []string{zone.Locality}, 316 | Province: []string{zone.Province}, 317 | } 318 | return entry, nil 319 | } 320 | } else { 321 | err = fmt.Errorf("Tried to get new access token but refresh token is empty") 322 | return entry, err 323 | } 324 | 325 | } else { 326 | return entry, err 327 | } 328 | } 329 | if err != nil { 330 | return entry, fmt.Errorf("could not read zone configuration: %s", err) 331 | } 332 | entry = roleEntry{ 333 | OU: zone.OrganizationalUnit, 334 | Organization: []string{zone.Organization}, 335 | Country: []string{zone.Country}, 336 | Locality: []string{zone.Locality}, 337 | Province: []string{zone.Province}, 338 | } 339 | return 340 | } 341 | 342 | func (b *backend) getPKIRoleEntry(ctx context.Context, storage logical.Storage, roleName string) (entry *roleEntry, err error) { 343 | //Update role since it's settings may be changed 344 | entry, err = b.getRole(ctx, storage, roleName) 345 | if err != nil { 346 | return entry, fmt.Errorf("Error getting role %v: %s\n", roleName, err) 347 | } 348 | return entry, nil 349 | } 350 | -------------------------------------------------------------------------------- /plugin/pki/path_venafi_policy_sync_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/vault/sdk/logical" 7 | "os" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var policyTPPData = map[string]interface{}{ 13 | "tpp_url": os.Getenv("TPP_URL"), 14 | "tpp_user": os.Getenv("TPP_USER"), 15 | "tpp_password": os.Getenv("TPP_PASSWORD"), 16 | "zone": os.Getenv("TPP_ZONE"), 17 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 18 | "venafi_secret": venafiSecretDefaultName + "Tpp", 19 | } 20 | 21 | var policyTPPData2 = map[string]interface{}{ 22 | "tpp_url": os.Getenv("TPP_URL"), 23 | "tpp_user": os.Getenv("TPP_USER"), 24 | "tpp_password": os.Getenv("TPP_PASSWORD"), 25 | "zone": os.Getenv("TPP_ZONE2"), 26 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 27 | "venafi_secret": venafiSecretDefaultName + "Tpp2", 28 | } 29 | 30 | var policyCloudData = map[string]interface{}{ 31 | "apikey": os.Getenv("CLOUD_APIKEY"), 32 | "cloud_url": os.Getenv("CLOUD_URL"), 33 | "zone": os.Getenv("CLOUD_ZONE_RESTRICTED"), 34 | "venafi_secret": venafiSecretDefaultName + "Cloud", 35 | } 36 | 37 | var wantTPPRoleEntry = roleEntry{ 38 | Organization: []string{"Venafi Inc."}, 39 | OU: []string{"Integrations"}, 40 | Locality: []string{"Salt Lake"}, 41 | Province: []string{"Utah"}, 42 | Country: []string{"US"}, 43 | AllowedDomains: []string{}, 44 | KeyUsage: []string{"CertSign"}, 45 | } 46 | 47 | var wantCloudRoleEntry = roleEntry{ 48 | Organization: []string{"Venafi Inc."}, 49 | OU: []string{"Integrations"}, 50 | Locality: []string{"Salt Lake"}, 51 | Province: []string{"Utah"}, 52 | Country: []string{"US"}, 53 | AllowedDomains: []string{}, 54 | KeyUsage: []string{"CertSign"}, 55 | } 56 | 57 | var wantTPPRoleEntry2 = roleEntry{ 58 | Organization: []string{"Venafi2"}, 59 | OU: []string{"Integrations2"}, 60 | Locality: []string{"Default"}, 61 | Province: []string{"Utah2"}, 62 | Country: []string{"FR"}, 63 | AllowedDomains: []string{}, 64 | KeyUsage: []string{"CertSign"}, 65 | } 66 | 67 | var wantTPPRoleEntryNoSync = roleEntry{ 68 | Organization: []string{"Default"}, 69 | OU: []string{"Default"}, 70 | Locality: []string{"Default"}, 71 | Province: []string{"Default"}, 72 | Country: []string{"Default"}, 73 | AllowedDomains: []string{"example.com"}, 74 | KeyUsage: []string{"CertSign"}, 75 | } 76 | 77 | var roleData = map[string]interface{}{ 78 | "organization": "Default", 79 | "ou": "Default", 80 | "locality": "Default", 81 | "province": "Default", 82 | "country": "Default", 83 | "allowed_domains": "example.com", 84 | "allow_subdomains": "true", 85 | "max_ttl": "4h", 86 | "key_usage": "CertSign", 87 | "allow_bare_domains": true, 88 | "generate_lease": true, 89 | } 90 | 91 | func TestSyncRoleWithTPPPolicy(t *testing.T) { 92 | // create the backend 93 | config := logical.TestBackendConfig() 94 | storage := &logical.InmemStorage{} 95 | testRoleName := "test-venafi-role" 96 | config.StorageView = storage 97 | policy := copyMap(policyTPPData2) 98 | b := Backend(config) 99 | err := b.Setup(context.Background(), config) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 105 | Operation: logical.UpdateOperation, 106 | Path: "roles/" + testRoleName, 107 | Storage: storage, 108 | Data: roleData, 109 | }) 110 | if resp != nil && resp.IsError() { 111 | t.Fatalf("failed to create a role, %#v", resp) 112 | } 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | //write TPP policy 118 | policy[policyFieldDefaultsRoles] = testRoleName 119 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 120 | 121 | ctx := context.Background() 122 | err = b.syncPolicyEnforcementAndRoleDefaults(config) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | roleEntryData, err := b.getPKIRoleEntry(ctx, storage, testRoleName) 128 | 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | if roleEntryData == nil { 134 | t.Fatal("role entry should not be nil") 135 | } 136 | 137 | t.Log("Checking modified role entry") 138 | checkRoleEntry(t, *roleEntryData, wantTPPRoleEntry2) 139 | } 140 | 141 | func TestIntegrationSyncRoleWithPolicy(t *testing.T) { 142 | // create the backend 143 | config := logical.TestBackendConfig() 144 | storage := &logical.InmemStorage{} 145 | testRoleName := "test-venafi-role" 146 | config.StorageView = storage 147 | policy := copyMap(policyTPPData) 148 | 149 | b := Backend(config) 150 | err := b.Setup(context.Background(), config) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | //write TPP policy 156 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 157 | 158 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 159 | Operation: logical.UpdateOperation, 160 | Path: "roles/" + testRoleName, 161 | Storage: storage, 162 | Data: roleData, 163 | }) 164 | if resp != nil && resp.IsError() { 165 | t.Fatalf("failed to create a role, %#v", resp) 166 | } 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | policy[policyFieldDefaultsRoles] = testRoleName 172 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 173 | 174 | ctx := context.Background() 175 | 176 | t.Log("Sleeping to wait while scheduler execute sync task") 177 | time.Sleep(25 * time.Second) 178 | 179 | t.Log("Checking role entry") 180 | roleEntryData, err := b.getPKIRoleEntry(ctx, storage, testRoleName) 181 | 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | 186 | if roleEntryData == nil { 187 | t.Fatal("role entry should not be nil") 188 | } 189 | 190 | t.Log("Checking modified role entry") 191 | checkRoleEntry(t, *roleEntryData, wantTPPRoleEntry) 192 | } 193 | 194 | func TestSyncRoleWithCloudPolicy(t *testing.T) { 195 | // create the backend 196 | config := logical.TestBackendConfig() 197 | storage := &logical.InmemStorage{} 198 | testRoleName := "test-venafi-role" 199 | config.StorageView = storage 200 | policy := copyMap(policyCloudData) 201 | 202 | b := Backend(config) 203 | err := b.Setup(context.Background(), config) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | //write TPP policy 209 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 210 | 211 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 212 | Operation: logical.UpdateOperation, 213 | Path: "roles/" + testRoleName, 214 | Storage: storage, 215 | Data: roleData, 216 | }) 217 | if resp != nil && resp.IsError() { 218 | t.Fatalf("failed to create a role, %#v", resp) 219 | } 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | policy[policyFieldDefaultsRoles] = testRoleName 225 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 226 | 227 | ctx := context.Background() 228 | err = b.syncPolicyEnforcementAndRoleDefaults(config) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | roleEntryData, err := b.getPKIRoleEntry(ctx, storage, testRoleName) 234 | 235 | if err != nil { 236 | t.Fatal(err) 237 | } 238 | 239 | if roleEntryData == nil { 240 | t.Fatal("role entry should not be nil") 241 | } 242 | 243 | t.Log("Checking modified role entry") 244 | checkRoleEntry(t, *roleEntryData, wantCloudRoleEntry) 245 | } 246 | 247 | func TestSyncMultipleRolesWithTPPPolicy(t *testing.T) { 248 | // create the backend 249 | config := logical.TestBackendConfig() 250 | storage := &logical.InmemStorage{} 251 | testRoleName := "test-venafi-role" 252 | config.StorageView = storage 253 | 254 | b := Backend(config) 255 | err := b.Setup(context.Background(), config) 256 | if err != nil { 257 | t.Fatal(err) 258 | } 259 | 260 | t.Log("Setting up first policy") 261 | 262 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 263 | Operation: logical.UpdateOperation, 264 | Path: "roles/" + testRoleName, 265 | Storage: storage, 266 | Data: roleData, 267 | }) 268 | if resp != nil && resp.IsError() { 269 | t.Fatalf("failed to create a role, %#v", resp) 270 | } 271 | if err != nil { 272 | t.Fatal(err) 273 | } 274 | 275 | t.Log("Setting up second role") 276 | writePolicy(b, storage, policyCloudData, t, "cloud-policy") 277 | 278 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 279 | Operation: logical.UpdateOperation, 280 | Path: "roles/" + testRoleName + "-second", 281 | Storage: storage, 282 | Data: roleData, 283 | }) 284 | if resp != nil && resp.IsError() { 285 | t.Fatalf("failed to create a role, %#v", resp) 286 | } 287 | if err != nil { 288 | t.Fatal(err) 289 | } 290 | 291 | t.Log("Setting up third role without sync") 292 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 293 | Operation: logical.UpdateOperation, 294 | Path: "roles/" + testRoleName + "-third", 295 | Storage: storage, 296 | Data: roleData, 297 | }) 298 | if resp != nil && resp.IsError() { 299 | t.Fatalf("failed to create a role, %#v", resp) 300 | } 301 | if err != nil { 302 | t.Fatal(err) 303 | } 304 | 305 | t.Log("Setting up fourth role") 306 | 307 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 308 | Operation: logical.UpdateOperation, 309 | Path: "roles/" + testRoleName + "-fourth", 310 | Storage: storage, 311 | Data: roleData, 312 | }) 313 | if resp != nil && resp.IsError() { 314 | t.Fatalf("failed to create a role, %#v", resp) 315 | } 316 | if err != nil { 317 | t.Fatal(err) 318 | } 319 | 320 | t.Log("Setting up policy") 321 | 322 | policy := copyMap(policyTPPData) 323 | policy[policyFieldDefaultsRoles] = fmt.Sprintf("%s,%s", testRoleName, testRoleName+"-second") 324 | writePolicy(b, storage, policy, t, "tpp-policy") 325 | 326 | policy2 := copyMap(policyTPPData2) 327 | policy2[policyFieldDefaultsRoles] = testRoleName + "-fourth" 328 | writePolicy(b, storage, policy2, t, "tpp2-policy") 329 | 330 | ctx := context.Background() 331 | err = b.syncPolicyEnforcementAndRoleDefaults(config) 332 | if err != nil { 333 | t.Fatal(err) 334 | } 335 | 336 | t.Log("Checking data for the first role") 337 | roleEntryData, err := b.getPKIRoleEntry(ctx, storage, testRoleName) 338 | 339 | if err != nil { 340 | t.Fatal(err) 341 | } 342 | 343 | if roleEntryData == nil { 344 | t.Fatal("role entry should not be nil") 345 | } 346 | 347 | checkRoleEntry(t, *roleEntryData, wantTPPRoleEntry) 348 | 349 | t.Log("Checking data for the second role") 350 | roleEntryData, err = b.getPKIRoleEntry(ctx, storage, testRoleName+"-second") 351 | 352 | if err != nil { 353 | t.Fatal(err) 354 | } 355 | 356 | if roleEntryData == nil { 357 | t.Fatal("role entry should not be nil") 358 | } 359 | 360 | checkRoleEntry(t, *roleEntryData, wantCloudRoleEntry) 361 | 362 | t.Log("Checking data for the third role") 363 | roleEntryData, err = b.getPKIRoleEntry(ctx, storage, testRoleName+"-third") 364 | 365 | if err != nil { 366 | t.Fatal(err) 367 | } 368 | 369 | if roleEntryData == nil { 370 | t.Fatal("role entry should not be nil") 371 | } 372 | 373 | checkRoleEntry(t, *roleEntryData, wantTPPRoleEntryNoSync) 374 | 375 | t.Log("Checking data for the fourth role") 376 | roleEntryData, err = b.getPKIRoleEntry(ctx, storage, testRoleName+"-fourth") 377 | 378 | if err != nil { 379 | t.Fatal(err) 380 | } 381 | 382 | if roleEntryData == nil { 383 | t.Fatal("role entry should not be nil") 384 | } 385 | 386 | checkRoleEntry(t, *roleEntryData, wantTPPRoleEntry2) 387 | 388 | // List roles with sync 389 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 390 | Operation: logical.ReadOperation, 391 | Path: venafiSyncPolicyListPath, 392 | Storage: storage, 393 | }) 394 | 395 | if resp != nil && resp.IsError() { 396 | t.Fatalf("failed to list roles, %#v", resp) 397 | } 398 | 399 | if err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | if resp.Data["keys"] == nil { 404 | t.Fatalf("Expected there will be roles in the keys list") 405 | } 406 | } 407 | 408 | func Test_backend_getPKIRoleEntry(t *testing.T) { 409 | // create the backend 410 | config := logical.TestBackendConfig() 411 | storage := &logical.InmemStorage{} 412 | config.StorageView = storage 413 | 414 | b := Backend(config) 415 | err := b.Setup(context.Background(), config) 416 | if err != nil { 417 | t.Fatal(err) 418 | } 419 | 420 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 421 | Operation: logical.UpdateOperation, 422 | Path: "roles/test-venafi-role", 423 | Storage: storage, 424 | Data: roleData, 425 | }) 426 | if resp != nil && resp.IsError() { 427 | t.Fatalf("failed to create a role, %#v", resp) 428 | } 429 | if err != nil { 430 | t.Fatal(err) 431 | } 432 | 433 | ctx := context.Background() 434 | entry, err := b.getPKIRoleEntry(ctx, storage, "test-venafi-role") 435 | if entry == nil { 436 | t.Fatal("role entry should not be nil") 437 | } 438 | var want string 439 | var have string 440 | 441 | want = roleData["organization"].(string) 442 | have = entry.Organization[0] 443 | if have != want { 444 | t.Fatalf("%s doesn't match %s", have, want) 445 | } 446 | 447 | want = roleData["ou"].(string) 448 | have = entry.OU[0] 449 | if have != want { 450 | t.Fatalf("%s doesn't match %s", have, want) 451 | } 452 | 453 | want = roleData["locality"].(string) 454 | have = entry.Locality[0] 455 | if have != want { 456 | t.Fatalf("%s doesn't match %s", have, want) 457 | } 458 | 459 | want = roleData["province"].(string) 460 | have = entry.Province[0] 461 | if have != want { 462 | t.Fatalf("%s doesn't match %s", have, want) 463 | } 464 | } 465 | 466 | func Test_backend_getVenafiPolicyParams(t *testing.T) { 467 | // create the backend 468 | config := logical.TestBackendConfig() 469 | storage := &logical.InmemStorage{} 470 | config.StorageView = storage 471 | 472 | b := Backend(config) 473 | err := b.Setup(context.Background(), config) 474 | if err != nil { 475 | t.Fatal(err) 476 | } 477 | 478 | //write TPP policy 479 | ctx := context.Background() 480 | 481 | writePolicy(b, storage, policyTPPData, t, defaultVenafiPolicyName) 482 | venafiPolicyEntry, err := b.getVenafiPolicyParams(ctx, storage, defaultVenafiPolicyName, policyTPPData["zone"].(string)) 483 | if err != nil { 484 | t.Fatal(err) 485 | } 486 | 487 | var want string 488 | var have string 489 | 490 | want = wantTPPRoleEntry.Organization[0] 491 | have = venafiPolicyEntry.Organization[0] 492 | if have != want { 493 | t.Fatalf("%s doesn't match %s", have, want) 494 | } 495 | 496 | want = wantTPPRoleEntry.OU[0] 497 | have = venafiPolicyEntry.OU[0] 498 | if have != want { 499 | t.Fatalf("%s doesn't match %s", have, want) 500 | } 501 | } 502 | 503 | func TestAutoRefresh(t *testing.T) { 504 | //TODO: make it 505 | } 506 | -------------------------------------------------------------------------------- /plugin/pki/path_venafi_secret.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/vault/sdk/framework" 7 | "github.com/hashicorp/vault/sdk/logical" 8 | ) 9 | 10 | const ( 11 | venafiSecretPath = "venafi/" 12 | venafiSecretDefaultName = "secret_" 13 | pathVenafiSecretsSynopsis = "Manage the Venafi secrets that can be created with this backend." 14 | pathVenafiSecretsDescription = "This path lets you manage the Venafi secrets that can be created with this backend." // #nosec G101 15 | pathVenafiSecretsListSynopsis = "List the existing Venafi secrets in this backend." 16 | pathVenafiSecretsListDescription = "Venafi secrets will be listed by the secret name." // #nosec G101 17 | tokenMode = `TPP Token (access_token, refresh_token)` // #nosec G101 18 | tppMode = `TPP Credentials (tpp_user, tpp_password)` 19 | cloudMode = `Cloud API Key (apikey)` 20 | errorMultiModeMessage = `can't specify both: %s and %s modes in the same venafi secret` 21 | errorTextURLEmpty = `"url" argument is required` 22 | errorTextZoneEmpty = `"zone" argument is required` 23 | errorTextInvalidMode = "invalid mode: apikey or tpp credentials or tpp access/refresh token required" 24 | ) 25 | 26 | var ( 27 | errorTextMixedTPPAndCloud = fmt.Sprintf(errorMultiModeMessage, tppMode, cloudMode) 28 | errorTextMixedTokenAndCloud = fmt.Sprintf(errorMultiModeMessage, tokenMode, cloudMode) 29 | ) 30 | 31 | func pathVenafiSecretsList(b *backend) *framework.Path { 32 | ret := &framework.Path{ 33 | Pattern: venafiSecretPath + "?$", 34 | Operations: map[logical.Operation]framework.OperationHandler{ 35 | logical.ListOperation: &framework.PathOperation{ 36 | Callback: b.pathListVenafiSecrets, 37 | Summary: "Return all venafi secrets.", 38 | }, 39 | }, 40 | HelpSynopsis: pathVenafiSecretsListSynopsis, 41 | HelpDescription: pathVenafiSecretsListDescription, 42 | } 43 | 44 | return ret 45 | } 46 | 47 | func pathVenafiSecrets(b *backend) *framework.Path { 48 | ret := &framework.Path{ 49 | Pattern: venafiSecretPath + framework.GenericNameRegex("name"), 50 | Fields: map[string]*framework.FieldSchema{ 51 | "name": { 52 | Type: framework.TypeString, 53 | Description: "Name of the Venafi secret.", 54 | }, 55 | "tpp_url": { 56 | Type: framework.TypeString, 57 | Description: `URL of Venafi Platform. Deprecated, use 'url' instead`, 58 | Deprecated: true, 59 | }, 60 | "url": { 61 | Type: framework.TypeString, 62 | Description: `URL of Venafi API endpoint. Example: https://tpp.venafi.example/vedsdk`, 63 | Required: true, 64 | }, 65 | "access_token": { 66 | Type: framework.TypeString, 67 | Description: `Access token for TPP, user should use this for authentication`, 68 | Required: true, 69 | }, 70 | "refresh_token": { 71 | Type: framework.TypeString, 72 | Description: `Refresh token for obtaining a new access token for TPP`, 73 | Required: true, 74 | }, 75 | "tpp_user": { 76 | Type: framework.TypeString, 77 | Description: `web API user for Venafi Platform Example: admin`, 78 | Required: true, 79 | }, 80 | "tpp_password": { 81 | Type: framework.TypeString, 82 | Description: `Password for web API user Example: password`, 83 | Required: true, 84 | DisplayAttrs: &framework.DisplayAttributes{ 85 | Sensitive: true, 86 | }, 87 | }, 88 | "apikey": { 89 | Type: framework.TypeString, 90 | Description: `API key for Venafi Cloud. Example: 142231b7-cvb0-412e-886b-6a1ght0bc93d`, 91 | DisplayAttrs: &framework.DisplayAttributes{ 92 | Sensitive: true, 93 | }, 94 | }, 95 | "cloud_url": { 96 | Type: framework.TypeString, 97 | Description: `URL for Venafi Cloud. Set it only if you want to use non production Cloud. Deprecated, use 'url' instead`, 98 | Deprecated: true, 99 | }, 100 | "zone": { 101 | Type: framework.TypeString, 102 | Description: `Name of Venafi Platform or Cloud policy. 103 | Example for Platform: testPolicy\\vault 104 | Example for Venafi Cloud: Default`, 105 | Default: `Default`, 106 | Required: true, 107 | }, 108 | "trust_bundle_file": { 109 | Type: framework.TypeString, 110 | Description: `Use to specify a PEM formatted file with certificates to be used as trust anchors when communicating with the remote server. 111 | Example: 112 | trust_bundle_file = "/full/path/to/chain.pem""`, 113 | }, 114 | }, 115 | Operations: map[logical.Operation]framework.OperationHandler{ 116 | logical.ReadOperation: &framework.PathOperation{ 117 | Callback: b.pathReadVenafiSecret, 118 | Summary: "Return the venafi resource specified in path.", 119 | }, 120 | logical.UpdateOperation: &framework.PathOperation{ 121 | Callback: b.pathUpdateVenafiSecret, 122 | Summary: "Configure a Venafi resource for use with the Venafi Policy.", 123 | }, 124 | logical.DeleteOperation: &framework.PathOperation{ 125 | Callback: b.pathDeleteVenafiSecret, 126 | Summary: "Removes the Venafi resource specified in path.", 127 | }, 128 | }, 129 | HelpSynopsis: pathVenafiSecretsSynopsis, 130 | HelpDescription: pathVenafiSecretsDescription, 131 | } 132 | 133 | return ret 134 | } 135 | 136 | func (b *backend) pathListVenafiSecrets(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 137 | entries, err := req.Storage.List(ctx, venafiSecretPath) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return logical.ListResponse(entries), nil 143 | } 144 | 145 | func (b *backend) pathReadVenafiSecret(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 146 | secretName := data.Get("name").(string) 147 | if len(secretName) == 0 { 148 | return logical.ErrorResponse("missing venafi secret name"), nil 149 | } 150 | 151 | cred, err := b.getVenafiSecret(ctx, &req.Storage, secretName) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | if cred == nil { 157 | return nil, nil 158 | } 159 | 160 | resp := &logical.Response{ 161 | Data: cred.ToResponseData(), 162 | } 163 | 164 | return resp, nil 165 | } 166 | 167 | func (b *backend) getVenafiSecret(ctx context.Context, s *logical.Storage, name string) (*venafiSecretEntry, error) { 168 | entry, err := (*s).Get(ctx, venafiSecretPath+name) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | if entry == nil { 174 | return nil, nil 175 | } 176 | 177 | var result venafiSecretEntry 178 | err = entry.DecodeJSON(&result) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | return &result, nil 184 | } 185 | 186 | func (b *backend) pathUpdateVenafiSecret(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 187 | var err error 188 | name := data.Get("name").(string) 189 | 190 | url := data.Get("url").(string) 191 | tppUrl := data.Get("tpp_url").(string) 192 | cloudUrl := data.Get("cloud_url").(string) 193 | 194 | if url == "" { 195 | 196 | if tppUrl != "" { 197 | url = tppUrl 198 | } else if cloudUrl != "" { 199 | url = cloudUrl 200 | } 201 | } 202 | 203 | entry := &venafiSecretEntry{ 204 | URL: url, 205 | Zone: data.Get("zone").(string), 206 | TPPUrl: tppUrl, 207 | TPPUser: data.Get("tpp_user").(string), 208 | TPPPassword: data.Get("tpp_password").(string), 209 | AccessToken: data.Get("access_token").(string), 210 | RefreshToken: data.Get("refresh_token").(string), 211 | CloudURL: cloudUrl, 212 | Apikey: data.Get("apikey").(string), 213 | TrustBundleFile: data.Get("trust_bundle_file").(string), 214 | } 215 | 216 | err = validateVenafiSecretEntry(entry) 217 | if err != nil { 218 | return logical.ErrorResponse(err.Error()), nil 219 | } 220 | 221 | if entry.RefreshToken != "" { 222 | 223 | cfg, err := createConfigFromFieldData(entry) 224 | 225 | if err != nil { 226 | 227 | return logical.ErrorResponse(err.Error()), nil 228 | 229 | } 230 | 231 | tokenInfo, err := getAccessData(cfg) 232 | 233 | if err != nil { 234 | 235 | return logical.ErrorResponse(err.Error()), nil 236 | 237 | } else { 238 | 239 | if tokenInfo.Access_token != "" { 240 | entry.AccessToken = tokenInfo.Access_token 241 | } 242 | 243 | if tokenInfo.Refresh_token != "" { 244 | entry.RefreshToken = tokenInfo.Refresh_token 245 | } 246 | 247 | } 248 | 249 | } 250 | 251 | jsonEntry, err := logical.StorageEntryJSON(venafiSecretPath+name, entry) 252 | if err != nil { 253 | return nil, err 254 | } 255 | 256 | err = req.Storage.Put(ctx, jsonEntry) 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | var logResp *logical.Response 262 | 263 | warnings := getWarnings(entry, name) 264 | 265 | if cap(warnings) > 0 { 266 | logResp = &logical.Response{ 267 | 268 | Data: map[string]interface{}{}, 269 | Redirect: "", 270 | Warnings: warnings, 271 | } 272 | return logResp, nil 273 | } 274 | 275 | return nil, nil 276 | } 277 | 278 | func (b *backend) pathDeleteVenafiSecret(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 279 | name := data.Get("name").(string) 280 | 281 | //Deleting secrets path 282 | err := req.Storage.Delete(ctx, venafiSecretPath+name) 283 | if err != nil { 284 | return nil, err 285 | } 286 | 287 | return nil, nil 288 | } 289 | 290 | func (c *venafiSecretEntry) ToResponseData() map[string]interface{} { 291 | responseData := map[string]interface{}{ 292 | "url": c.URL, 293 | "zone": c.Zone, 294 | "tpp_user": c.TPPUser, 295 | "tpp_password": c.getMaskString(), 296 | "access_token": c.getMaskString(), 297 | "refresh_token": c.getMaskString(), 298 | "apikey": c.getMaskString(), 299 | "trust_bundle_file": c.TrustBundleFile, 300 | } 301 | return responseData 302 | } 303 | 304 | func getWarnings(c *venafiSecretEntry, name string) []string { 305 | 306 | var warnings []string 307 | 308 | if c.TPPUrl != "" { 309 | warnings = append(warnings, "tpp_url is deprecated, please use url instead") 310 | } 311 | 312 | if c.CloudURL != "" { 313 | warnings = append(warnings, "cloud_url is deprecated, please use url instead") 314 | } 315 | 316 | if c.TPPUser != "" { 317 | warnings = append(warnings, "tpp_user is deprecated, please use access_token instead") 318 | } 319 | 320 | if c.TPPPassword != "" { 321 | warnings = append(warnings, "tpp_password is deprecated, please use access_token instead") 322 | } 323 | 324 | //Include success message in warnings 325 | if len(warnings) > 0 { 326 | warnings = append(warnings, "Venafi secret: "+name+" saved successfully") 327 | } 328 | 329 | return warnings 330 | } 331 | 332 | func validateVenafiSecretEntry(entry *venafiSecretEntry) error { 333 | if entry.Apikey == "" && (entry.TPPUser == "" || entry.TPPPassword == "") && entry.AccessToken == "" && entry.RefreshToken == "" { 334 | return fmt.Errorf(errorTextInvalidMode) 335 | } 336 | 337 | //When api key is null, that means TPP is being used, and requires a URL 338 | if entry.URL == "" && entry.Apikey == "" { 339 | return fmt.Errorf(errorTextURLEmpty) 340 | } 341 | 342 | if entry.Zone == "" { 343 | return fmt.Errorf(errorTextZoneEmpty) 344 | } 345 | 346 | if entry.TPPUser != "" && entry.Apikey != "" { 347 | return fmt.Errorf(errorTextMixedTPPAndCloud) 348 | } 349 | 350 | if entry.AccessToken != "" && entry.Apikey != "" { 351 | return fmt.Errorf(errorTextMixedTokenAndCloud) 352 | } 353 | 354 | return nil 355 | } 356 | -------------------------------------------------------------------------------- /plugin/pki/path_venafi_secret_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import "testing" 4 | 5 | func TestVenafiSecretValidate(t *testing.T) { 6 | entry := &venafiSecretEntry{} 7 | 8 | err := validateVenafiSecretEntry(entry) 9 | if err == nil { 10 | t.Fatalf("Expecting error") 11 | } 12 | if err.Error() != errorTextInvalidMode { 13 | t.Fatalf("Expecting error %s but got %s", errorTextInvalidMode, err) 14 | } 15 | 16 | entry = &venafiSecretEntry{ 17 | AccessToken: "foo123bar==", 18 | } 19 | 20 | err = validateVenafiSecretEntry(entry) 21 | if err == nil { 22 | t.Fatalf("Expecting error") 23 | } 24 | if err.Error() != errorTextURLEmpty { 25 | t.Fatalf("Expecting error %s but got %s", errorTextURLEmpty, err) 26 | } 27 | 28 | entry = &venafiSecretEntry{ 29 | URL: "https://qa-tpp.exmple.com/vedsdk", 30 | Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 31 | TPPUser: "admin", 32 | TPPPassword: "xxxx", 33 | Zone: "zoneName", 34 | } 35 | 36 | err = validateVenafiSecretEntry(entry) 37 | if err == nil { 38 | t.Fatalf("Expecting error") 39 | } 40 | if err.Error() != errorTextMixedTPPAndCloud { 41 | t.Fatalf("Expecting error %s but got %s", errorTextMixedTPPAndCloud, err) 42 | } 43 | 44 | entry = &venafiSecretEntry{ 45 | URL: "https://qa-tpp.exmple.com/vedsdk", 46 | AccessToken: "foo123bar==", 47 | Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 48 | Zone: "zoneName", 49 | } 50 | 51 | err = validateVenafiSecretEntry(entry) 52 | if err == nil { 53 | t.Fatalf("Expecting error") 54 | } 55 | if err.Error() != errorTextMixedTokenAndCloud { 56 | t.Fatalf("Expecting error %s but got %s", errorTextMixedTokenAndCloud, err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugin/pki/scheduler.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | type backgroundTask struct { 11 | name string 12 | f func() 13 | workers int64 14 | currentWorkers int64 15 | interval time.Duration 16 | lastRun time.Time 17 | } 18 | 19 | type taskStorageStruct struct { 20 | inited bool 21 | tasks []*backgroundTask 22 | sync.RWMutex 23 | } 24 | 25 | func (task *backgroundTask) cancel() { 26 | 27 | } 28 | 29 | func (s *taskStorageStruct) getTasksNames() []string { 30 | s.RLock() 31 | defer s.RUnlock() 32 | l := make([]string, len(s.tasks)) 33 | for i := range s.tasks { 34 | l[i] = s.tasks[i].name 35 | } 36 | return l 37 | } 38 | 39 | func (s *taskStorageStruct) register(name string, f func(), count int, interval time.Duration) { 40 | s.Lock() 41 | defer s.Unlock() 42 | task := backgroundTask{name: name, f: f, workers: int64(count), interval: interval} 43 | for i := range s.tasks { 44 | if s.tasks[i].name == task.name { 45 | log.Printf("%s duplicated task %v", logPrefixVenafiScheduler, name) 46 | return 47 | } 48 | } 49 | s.tasks = append(s.tasks, &task) 50 | } 51 | 52 | func (s *taskStorageStruct) del(taskName string) { 53 | s.Lock() 54 | defer s.Unlock() 55 | for i := range s.tasks { 56 | if s.tasks[i].name == taskName { 57 | s.tasks[i].cancel() 58 | s.tasks = append(s.tasks[:i], s.tasks[i+1:]...) 59 | return 60 | } 61 | } 62 | } 63 | 64 | func (s *taskStorageStruct) init() { 65 | if s.inited { 66 | panic(logPrefixVenafiScheduler + " twice inited loop") 67 | } 68 | s.inited = true 69 | go s.loop() 70 | } 71 | 72 | func (s *taskStorageStruct) loop() { 73 | for { 74 | s.RLock() 75 | for i := range s.tasks { 76 | currentTask := s.tasks[i] 77 | if currentTask.currentWorkers >= currentTask.workers { 78 | continue 79 | } 80 | if time.Since(currentTask.lastRun) < currentTask.interval { 81 | continue 82 | } 83 | atomic.AddInt64(¤tTask.currentWorkers, 1) 84 | go func(counter *int64) { 85 | defer func(counter *int64) { 86 | r := recover() 87 | if r != nil { 88 | log.Printf("%s job %s failed. recover: %v\n", logPrefixVenafiScheduler, currentTask.name, r) 89 | //todo: better log 90 | } 91 | atomic.AddInt64(counter, -1) 92 | }(counter) 93 | currentTask.f() 94 | }(¤tTask.currentWorkers) 95 | currentTask.lastRun = time.Now() 96 | } 97 | s.RUnlock() 98 | time.Sleep(time.Second) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /plugin/pki/scheduler_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func Test_scheduler_register(t *testing.T) { 11 | s := taskStorageStruct{} 12 | f1 := func() { fmt.Println("a") } 13 | f2 := func() { fmt.Println("b") } 14 | s.register("a", f1, 1, time.Second) 15 | if len(s.tasks) != 1 { 16 | t.Fatal("should be one task") 17 | } 18 | s.register("b", f2, 4, time.Minute) 19 | if len(s.tasks) != 2 { 20 | t.Fatal("should be two tasks") 21 | } 22 | s.register("c", f1, 1, time.Second) 23 | if len(s.tasks) != 3 { 24 | t.Fatal("should be three tasks") 25 | } 26 | s.register("a", f1, 5, time.Hour) 27 | if len(s.tasks) != 3 { 28 | t.Fatal("should be three tasks") 29 | } 30 | for i, n := range []string{"a", "b", "c"} { 31 | if s.tasks[i].name != n { 32 | t.Fatalf("name should be %v", n) 33 | } 34 | } 35 | for i, n := range []int64{1, 4, 1} { 36 | if s.tasks[i].workers != n { 37 | t.Fatalf("workers should be %v", n) 38 | } 39 | } 40 | } 41 | 42 | func Test_scheduler_del(t *testing.T) { 43 | s := taskStorageStruct{} 44 | f1 := func() { fmt.Println("a") } 45 | s.del("0") 46 | if len(s.tasks) != 0 { 47 | t.Fatal("should be zero tasks") 48 | } 49 | s.register("1", f1, 1, time.Second) 50 | s.del("0") 51 | if len(s.tasks) != 1 { 52 | t.Fatal("should be one task") 53 | } 54 | s.del("1") 55 | if len(s.tasks) != 0 { 56 | t.Fatal("should be zero tasks") 57 | } 58 | s.register("2", f1, 1, time.Second) 59 | s.register("3", f1, 1, time.Second) 60 | s.register("4", f1, 1, time.Second) 61 | s.register("5", f1, 1, time.Second) 62 | s.register("6", f1, 1, time.Second) 63 | s.del("2") 64 | s.del("4") 65 | s.del("6") 66 | if len(s.tasks) != 2 { 67 | t.Fatal("should be two tasks") 68 | } 69 | if s.tasks[0].name != "3" || s.tasks[1].name != "5" { 70 | t.Fatal("incorrect tasks was deleted") 71 | } 72 | } 73 | 74 | func Test_scheduler_concurency(t *testing.T) { 75 | s := taskStorageStruct{} 76 | const threads = 100 77 | const iterations = 1000 78 | s.init() 79 | var globalCounter int64 80 | for i := 0; i < threads; i++ { 81 | go func(i int) { 82 | for j := 0; j < iterations; j++ { 83 | if j%10 == 0 { 84 | s.del(fmt.Sprintf("task-%v-%v", i, j-1)) 85 | } 86 | s.register(fmt.Sprintf("task-%v-%v", i, j), func() { 87 | atomic.AddInt64(&globalCounter, 1) 88 | }, 1, time.Hour) 89 | } 90 | 91 | }(i) 92 | } 93 | time.Sleep(time.Minute * 2) 94 | tasksCount := threads*iterations*9/10 + threads 95 | if len(s.tasks) != tasksCount { 96 | t.Fatalf("tasks count should be %v but it is %v", tasksCount, len(s.tasks)) 97 | } 98 | if globalCounter < int64(tasksCount) || globalCounter > threads*iterations { 99 | t.Fatalf("something wrong with incrementer value: %v. should be between %v and %v", globalCounter, tasksCount, threads*iterations) 100 | } 101 | } 102 | 103 | func Test_scheduler_running(t *testing.T) { 104 | s := taskStorageStruct{} 105 | const iterations = 1000 106 | var globalCounter int64 107 | s.init() 108 | for i := 0; i < iterations; i++ { 109 | s.register(fmt.Sprintf("task-%v", i), func() { 110 | atomic.AddInt64(&globalCounter, 1) 111 | }, 1, time.Second*10) 112 | } 113 | time.Sleep(time.Second * 28) 114 | if globalCounter != iterations*3 { 115 | t.Fatalf("global counter should be %v but it is %v", iterations*3, globalCounter) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /plugin/pki/secret_certs.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/hashicorp/vault/sdk/framework" 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | // SecretCertsType is the name used to identify this type 12 | const SecretCertsType = "pki" 13 | 14 | func secretCerts(b *backend) *framework.Secret { 15 | return &framework.Secret{ 16 | Type: SecretCertsType, 17 | Fields: map[string]*framework.FieldSchema{ 18 | "certificate": &framework.FieldSchema{ 19 | Type: framework.TypeString, 20 | Description: `The PEM-encoded concatenated certificate and 21 | issuing certificate authority`, 22 | }, 23 | "private_key": &framework.FieldSchema{ 24 | Type: framework.TypeString, 25 | Description: "The PEM-encoded private key for the certificate", 26 | }, 27 | "serial": &framework.FieldSchema{ 28 | Type: framework.TypeString, 29 | Description: `The serial number of the certificate, for handy 30 | reference`, 31 | }, 32 | }, 33 | 34 | Revoke: b.secretCredsRevoke, 35 | } 36 | } 37 | 38 | func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 39 | if req.Secret == nil { 40 | return nil, fmt.Errorf("secret is nil in request") 41 | } 42 | 43 | serialInt, ok := req.Secret.InternalData["serial_number"] 44 | if !ok { 45 | return nil, fmt.Errorf("could not find serial in internal secret data") 46 | } 47 | 48 | b.revokeStorageLock.Lock() 49 | defer b.revokeStorageLock.Unlock() 50 | 51 | return revokeCert(ctx, b, req, serialInt.(string), true) 52 | } 53 | -------------------------------------------------------------------------------- /plugin/pki/util.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/asn1" 8 | "fmt" 9 | "github.com/Venafi/vcert/v4" 10 | "github.com/Venafi/vcert/v4/pkg/certificate" 11 | "github.com/Venafi/vcert/v4/pkg/endpoint" 12 | "github.com/Venafi/vcert/v4/pkg/venafi/tpp" 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "io/ioutil" 15 | "net" 16 | "net/http" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | const ( 24 | HTTP_UNAUTHORIZED = 401 25 | ) 26 | func normalizeSerial(serial string) string { 27 | return strings.Replace(strings.ToLower(serial), ":", "-", -1) 28 | } 29 | 30 | func parseExtKeyUsageParameter(unparsed []string) ([]x509.ExtKeyUsage, error) { 31 | extKeyUsages := make([]x509.ExtKeyUsage, 0, len(unparsed)) 32 | oidRegexp := regexp.MustCompile(`(\d+\.)+\d`) 33 | idRegexp := regexp.MustCompile(`\d+`) 34 | stringRegexp := regexp.MustCompile(`[a-z]+`) 35 | for _, s := range unparsed { 36 | switch { 37 | case oidRegexp.MatchString(s): 38 | oid, _ := stringToOid(s) 39 | eku, ok := extKeyUsageFromOID(oid) 40 | if !ok { 41 | return nil, fmt.Errorf("unknow oid: %s", s) 42 | } 43 | extKeyUsages = append(extKeyUsages, eku) 44 | case idRegexp.MatchString(s): 45 | eku, err := ekuParse(s) 46 | if err != nil { 47 | return nil, err 48 | } 49 | extKeyUsages = append(extKeyUsages, eku) 50 | case stringRegexp.MatchString(s): 51 | eku, known := findEkuByName(s) 52 | if !known { 53 | return nil, fmt.Errorf("unknown eku: %s", s) 54 | } 55 | extKeyUsages = append(extKeyUsages, eku) 56 | default: 57 | return nil, fmt.Errorf("unknow extKeyUsage format: %s", s) 58 | } 59 | } 60 | return extKeyUsages, nil 61 | } 62 | 63 | var ( 64 | oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} 65 | oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} 66 | oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} 67 | oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} 68 | oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} 69 | oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} 70 | oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} 71 | oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} 72 | oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} 73 | oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} 74 | oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} 75 | oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} 76 | oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} 77 | oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} 78 | ) 79 | 80 | var extKeyUsageOIDs = []struct { 81 | extKeyUsage x509.ExtKeyUsage 82 | oid asn1.ObjectIdentifier 83 | name string 84 | }{ 85 | {x509.ExtKeyUsageAny, oidExtKeyUsageAny, "any"}, 86 | {x509.ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth, "serverauth"}, 87 | {x509.ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth, "clientauth"}, 88 | {x509.ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning, "codesigning"}, 89 | {x509.ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection, "emailprotection"}, 90 | {x509.ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem, "ipsecendsystem"}, 91 | {x509.ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel, "ipsectunnel"}, 92 | {x509.ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser, "ipsecuser"}, 93 | {x509.ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping, "timestamping"}, 94 | {x509.ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning, "ocspsigning"}, 95 | {x509.ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto, "microsoftservergatedcrypto"}, 96 | {x509.ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto, "netscapeservergatedcrypto"}, 97 | {x509.ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning, "microsoftcommercialcodesigning"}, 98 | {x509.ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning, "microsoftkernelcodesigning"}, 99 | } 100 | 101 | func extKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku x509.ExtKeyUsage, ok bool) { 102 | for _, triplet := range extKeyUsageOIDs { 103 | if oid.Equal(triplet.oid) { 104 | return triplet.extKeyUsage, true 105 | } 106 | } 107 | return 108 | } 109 | 110 | func checkExtKeyUsage(eku x509.ExtKeyUsage) bool { 111 | for _, triplet := range extKeyUsageOIDs { 112 | if triplet.extKeyUsage == eku { 113 | return true 114 | } 115 | } 116 | return false 117 | } 118 | 119 | func findEkuByName(name string) (x509.ExtKeyUsage, bool) { 120 | name = strings.ToLower(name) 121 | for _, triplet := range extKeyUsageOIDs { 122 | if triplet.name == name { 123 | return triplet.extKeyUsage, true 124 | } 125 | } 126 | return 0, false 127 | } 128 | func ekuParse(s string) (eku x509.ExtKeyUsage, err error) { 129 | i, _ := strconv.Atoi(s) 130 | eku = x509.ExtKeyUsage(i) 131 | if checkExtKeyUsage(eku) { 132 | return 133 | } 134 | err = fmt.Errorf("unknow eku: %s", s) 135 | return 136 | } 137 | 138 | func ekuInSlice(i x509.ExtKeyUsage, s []x509.ExtKeyUsage) bool { 139 | for _, j := range s { 140 | if j == i { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | func compareEkuList(target, allowed []x509.ExtKeyUsage) bool { 147 | if len(allowed) == 0 { 148 | return true 149 | } 150 | for _, i := range target { 151 | if !ekuInSlice(i, allowed) { 152 | return false 153 | } 154 | } 155 | return true 156 | } 157 | 158 | func intInSlice(i int, s []int) bool { 159 | for _, j := range s { 160 | if i == j { 161 | return true 162 | } 163 | } 164 | return false 165 | } 166 | 167 | func curveInSlice(i certificate.EllipticCurve, s []certificate.EllipticCurve) bool { 168 | for _, j := range s { 169 | if i == j { 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | 176 | func checkKey(keyType string, bitSize int, curveStr string, allowed []endpoint.AllowedKeyConfiguration) (valid bool) { 177 | for _, allowedKey := range allowed { 178 | var kt certificate.KeyType 179 | if err := kt.Set(keyType); err != nil { 180 | return false 181 | } 182 | if allowedKey.KeyType == kt { 183 | switch allowedKey.KeyType { 184 | case certificate.KeyTypeRSA: 185 | return intInSlice(bitSize, allowedKey.KeySizes) 186 | case certificate.KeyTypeECDSA: 187 | var curve certificate.EllipticCurve 188 | if err := curve.Set(curveStr); err != nil { 189 | return false 190 | } 191 | return curveInSlice(curve, allowedKey.KeyCurves) 192 | default: 193 | return 194 | } 195 | } 196 | } 197 | return 198 | } 199 | 200 | func checkStringByRegexp(s string, regexList []string) (matched bool) { 201 | var err error 202 | for _, r := range regexList { 203 | matched, err = regexp.MatchString(r, s) 204 | if err == nil && matched { 205 | return true 206 | } 207 | } 208 | return 209 | } 210 | 211 | func checkStringArrByRegexp(ss []string, regexList []string, optional bool) (matched bool) { 212 | if optional && len(ss) == 0 { 213 | return true 214 | } 215 | if len(ss) == 0 { 216 | ss = []string{""} 217 | } 218 | for _, s := range ss { 219 | if !checkStringByRegexp(s, regexList) { 220 | return false 221 | } 222 | } 223 | return true 224 | } 225 | 226 | func ecdsaCurvesSizesToName(bitLen int) string { 227 | return fmt.Sprintf("P%d", bitLen) 228 | } 229 | 230 | func getTppConnector(cfg *vcert.Config) (*tpp.Connector, error) { 231 | 232 | var connectionTrustBundle *x509.CertPool 233 | if cfg.ConnectionTrust != "" { 234 | connectionTrustBundle = x509.NewCertPool() 235 | if !connectionTrustBundle.AppendCertsFromPEM([]byte(cfg.ConnectionTrust)) { 236 | return nil, fmt.Errorf("failed to parse PEM trust bundle") 237 | } 238 | } 239 | tppConnector, err := tpp.NewConnector(cfg.BaseUrl, "", cfg.LogVerbose, connectionTrustBundle) 240 | if err != nil { 241 | return nil, fmt.Errorf("could not create TPP connector: %s", err) 242 | } 243 | 244 | return tppConnector, nil 245 | } 246 | 247 | func synchronizedUpdateAccessToken(cfg *vcert.Config, b *backend, ctx context.Context, storage *logical.Storage, policyConfigName string) error { 248 | b.mux.Lock() 249 | err := updateAccessToken(cfg, b, ctx, storage, policyConfigName) 250 | b.mux.Unlock() 251 | return err 252 | } 253 | 254 | func updateAccessToken(cfg *vcert.Config, b *backend, ctx context.Context, storage *logical.Storage, policyConfigName string) error { 255 | tppConnector, _ := getTppConnector(cfg) 256 | 257 | httpClient, err := getHTTPClient(cfg.ConnectionTrust) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | tppConnector.SetHTTPClient(httpClient) 263 | 264 | resp, err := tppConnector.RefreshAccessToken(&endpoint.Authentication{ 265 | RefreshToken: cfg.Credentials.RefreshToken, 266 | ClientId: "hashicorp-vault-monitor-by-venafi", 267 | Scope: "certificate:discover,manage", 268 | }) 269 | 270 | if err != nil { 271 | return err 272 | } 273 | 274 | if resp.Access_token != "" && resp.Refresh_token != "" { 275 | 276 | err := storeAccessData(b, ctx, storage, policyConfigName, resp) 277 | if err != nil { 278 | return err 279 | } 280 | 281 | } 282 | 283 | return nil 284 | } 285 | 286 | func storeAccessData(b *backend, ctx context.Context, storage *logical.Storage, policyName string, resp tpp.OauthRefreshAccessTokenResponse) error { 287 | policy, err := b.getVenafiPolicyConfig(ctx, storage, policyName) 288 | 289 | if err != nil { 290 | return err 291 | } 292 | 293 | secret, err := b.getVenafiSecret(ctx, storage, policy.VenafiSecret) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | secret.RefreshToken = resp.Refresh_token 299 | 300 | secret.AccessToken = resp.Access_token 301 | 302 | // Store it 303 | jsonEntry, err := logical.StorageEntryJSON(venafiSecretPath+policy.VenafiSecret, secret) 304 | if err != nil { 305 | return err 306 | } 307 | 308 | if err := (*storage).Put(ctx, jsonEntry); err != nil { 309 | return err 310 | } 311 | 312 | //save the new credential on the backend storage. 313 | storageB := b.storage 314 | if err := storageB.Put(ctx, jsonEntry); err != nil { 315 | return err 316 | } 317 | 318 | return nil 319 | } 320 | 321 | func getHTTPClient(trustBundlePem string) (*http.Client, error) { 322 | 323 | var netTransport = &http.Transport{ 324 | Proxy: http.ProxyFromEnvironment, 325 | DialContext: (&net.Dialer{ 326 | Timeout: 30 * time.Second, 327 | KeepAlive: 30 * time.Second, 328 | }).DialContext, 329 | MaxIdleConns: 100, 330 | IdleConnTimeout: 90 * time.Second, 331 | TLSHandshakeTimeout: 10 * time.Second, 332 | ExpectContinueTimeout: 1 * time.Second, 333 | } 334 | 335 | tlsConfig := http.DefaultTransport.(*http.Transport).TLSClientConfig 336 | 337 | if tlsConfig == nil { 338 | /* #nosec */ 339 | tlsConfig = &tls.Config{} 340 | } else { 341 | tlsConfig = tlsConfig.Clone() 342 | } 343 | 344 | if trustBundlePem != "" { 345 | trustBundle, err := parseTrustBundlePEM(trustBundlePem) 346 | if err != nil { 347 | return nil, err 348 | } 349 | tlsConfig.RootCAs = trustBundle 350 | } 351 | 352 | tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient 353 | netTransport.TLSClientConfig = tlsConfig 354 | 355 | client := &http.Client{ 356 | Timeout: time.Second * 30, 357 | Transport: netTransport, 358 | } 359 | 360 | return client, nil 361 | } 362 | 363 | func parseTrustBundlePEM(trustBundlePem string) (*x509.CertPool, error) { 364 | var connectionTrustBundle *x509.CertPool 365 | 366 | if trustBundlePem != "" { 367 | connectionTrustBundle = x509.NewCertPool() 368 | if !connectionTrustBundle.AppendCertsFromPEM([]byte(trustBundlePem)) { 369 | return nil, fmt.Errorf("failed to parse PEM trust bundle") 370 | } 371 | } else { 372 | return nil, fmt.Errorf("trust bundle PEM data is empty") 373 | } 374 | 375 | return connectionTrustBundle, nil 376 | } 377 | 378 | func getStatusCode(msg string) int64 { 379 | 380 | var statusCode int64 381 | splittedMsg := strings.Split(msg, ":") 382 | 383 | for i := 0; i < len(splittedMsg); i++ { 384 | 385 | current := splittedMsg[i] 386 | current = strings.TrimSpace(current) 387 | 388 | if current == "Invalid status" { 389 | 390 | status := splittedMsg[i+1] 391 | status = strings.TrimSpace(status) 392 | splittedStatus := strings.Split(status, " ") 393 | statusCode, _ = strconv.ParseInt(splittedStatus[0], 10, 64) 394 | break 395 | 396 | } 397 | } 398 | 399 | return statusCode 400 | } 401 | 402 | func createConfigFromFieldData(data *venafiSecretEntry) (*vcert.Config, error) { 403 | var cfg = &vcert.Config{} 404 | 405 | cfg.BaseUrl = data.URL 406 | cfg.Zone = data.Zone 407 | cfg.LogVerbose = true 408 | 409 | trustBundlePath := data.TrustBundleFile 410 | 411 | if trustBundlePath != "" { 412 | 413 | var trustBundlePEM string 414 | trustBundle, err := ioutil.ReadFile(trustBundlePath) 415 | 416 | if err != nil { 417 | return cfg, err 418 | } 419 | 420 | trustBundlePEM = string(trustBundle) 421 | cfg.ConnectionTrust = trustBundlePEM 422 | } 423 | 424 | cfg.ConnectorType = endpoint.ConnectorTypeTPP 425 | 426 | cfg.Credentials = &endpoint.Authentication{ 427 | 428 | AccessToken: data.AccessToken, 429 | RefreshToken: data.RefreshToken, 430 | } 431 | 432 | return cfg, nil 433 | } 434 | 435 | func getAccessData(cfg *vcert.Config) (tpp.OauthRefreshAccessTokenResponse, error) { 436 | 437 | var tokenInfoResponse tpp.OauthRefreshAccessTokenResponse 438 | tppConnector, _ := getTppConnector(cfg) 439 | httpClient, err := getHTTPClient(cfg.ConnectionTrust) 440 | 441 | if err != nil { 442 | return tokenInfoResponse, err 443 | } 444 | 445 | tppConnector.SetHTTPClient(httpClient) 446 | 447 | tokenInfoResponse, err = tppConnector.RefreshAccessToken(&endpoint.Authentication{ 448 | RefreshToken: cfg.Credentials.RefreshToken, 449 | ClientId: "hashicorp-vault-monitor-by-venafi", 450 | Scope: "certificate:discover,manage", 451 | }) 452 | 453 | return tokenInfoResponse, err 454 | 455 | } -------------------------------------------------------------------------------- /plugin/pki/util_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func Test_checkStringArrByRegexp(t *testing.T) { 10 | cases := []struct { 11 | values []string 12 | regexps []string 13 | optional bool 14 | match bool 15 | }{ 16 | {[]string{}, []string{`.*`}, false, true}, 17 | {[]string{}, []string{}, false, false}, 18 | {[]string{}, []string{`RU`, `.*`}, false, true}, 19 | {[]string{}, []string{`.*`, `US`}, false, true}, 20 | {[]string{}, []string{`US`}, false, false}, 21 | {[]string{"US"}, []string{`US`}, false, true}, 22 | {[]string{"US"}, []string{`.*`}, false, true}, 23 | {[]string{"US", "RU"}, []string{`US`, `RU`}, false, true}, 24 | {[]string{"US", "GB"}, []string{`US`, `RU`}, false, false}, 25 | {[]string{"test.vfidev.com"}, []string{`.*\.vfidev\.com`}, false, true}, 26 | 27 | {[]string{}, []string{`.*`}, true, true}, 28 | {[]string{}, []string{}, true, true}, 29 | {[]string{}, []string{`RU`, `.*`}, true, true}, 30 | {[]string{}, []string{`.*`, `US`}, true, true}, 31 | {[]string{}, []string{`US`}, true, true}, 32 | {[]string{"US"}, []string{`US`}, true, true}, 33 | {[]string{"US"}, []string{`.*`}, true, true}, 34 | {[]string{"US", "RU"}, []string{`US`, `RU`}, true, true}, 35 | {[]string{"US", "GB"}, []string{`US`, `RU`}, true, false}, 36 | {[]string{"test.vfidev.com"}, []string{`.*\.vfidev\.com`}, true, true}, 37 | } 38 | for _, c := range cases { 39 | if checkStringArrByRegexp(c.values, c.regexps, c.optional) != c.match { 40 | t.Errorf("not valid %+v", c) 41 | } 42 | } 43 | } 44 | 45 | func randSeq(n int) string { 46 | rand.Seed(time.Now().UTC().UnixNano()) 47 | var letters = []rune("abcdefghijklmnopqrstuvwxyz1234567890") 48 | b := make([]rune, n) 49 | for i := range b { 50 | b[i] = letters[rand.Intn(len(letters))] 51 | } 52 | return string(b) 53 | } 54 | -------------------------------------------------------------------------------- /plugin/pki/vcert.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/Venafi/vcert/v4" 8 | "github.com/Venafi/vcert/v4/pkg/endpoint" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "io/ioutil" 11 | "log" 12 | ) 13 | 14 | //Set it false to disable Venafi policy check. It can be done only on the code level of the plugin. 15 | const venafiPolicyCheck = true 16 | var venafiPolicyDenyAll = true 17 | 18 | func (b *backend) ClientVenafi(ctx context.Context, s *logical.Storage, policyName string) ( 19 | endpoint.Connector, error) { 20 | 21 | if policyName == "" { 22 | return nil, fmt.Errorf("empty policy name") 23 | } 24 | 25 | config, err := b.getVenafiPolicyConfig(ctx, s, policyName) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if config == nil { 30 | return nil, fmt.Errorf("expected policy but got nil from Vault storage %v", config) 31 | } 32 | if config.VenafiSecret == "" { 33 | return nil, fmt.Errorf("empty Venafi secret name") 34 | } 35 | 36 | secret, err := b.getVenafiSecret(ctx, s, config.VenafiSecret) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if secret == nil { 41 | return nil, fmt.Errorf("expected Venafi secret but got nil from Vault storage %v", secret) 42 | } 43 | 44 | if config.Zone != "" { 45 | b.Logger().Debug("Using zone from Venafi Policy.", "zone", config.Zone) 46 | } else { 47 | b.Logger().Debug("Using zone from Venafi secret since Policy zone not found.", "zone", secret.Zone) 48 | } 49 | 50 | return secret.getConnection(config.Zone) 51 | } 52 | 53 | func (b *backend) getConfig(ctx context.Context, s *logical.Storage, policyName string) ( 54 | *vcert.Config, error) { 55 | 56 | if policyName == "" { 57 | return nil, fmt.Errorf("empty policy name") 58 | } 59 | 60 | config, err := b.getVenafiPolicyConfig(ctx, s, policyName) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if config == nil { 65 | return nil, fmt.Errorf("expected Policy config but got nil from Vault storage %v", config) 66 | } 67 | if config.VenafiSecret == "" { 68 | return nil, fmt.Errorf("empty Venafi secret name") 69 | } 70 | 71 | secret, err := b.getVenafiSecret(ctx, s, config.VenafiSecret) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if secret == nil { 76 | return nil, fmt.Errorf("expected Venafi secret but got nil from Vault storage %v", secret) 77 | } 78 | 79 | if config.Zone != "" { 80 | b.Logger().Debug("Using zone [%s] from Policy.", config.Zone) 81 | } else { 82 | b.Logger().Debug("Using zone [%s] from venafi secret. Policy zone not found.", secret.Zone) 83 | } 84 | 85 | return secret.getConfig(config.Zone, true) 86 | } 87 | 88 | func pp(a interface{}) string { 89 | b, err := json.MarshalIndent(a, "", " ") 90 | if err != nil { 91 | fmt.Println("error:", err) 92 | } 93 | return fmt.Sprint(string(b)) 94 | } 95 | 96 | type venafiSecretEntry struct { 97 | TPPUrl string `json:"tpp_url"` 98 | URL string `json:"url"` 99 | AccessToken string `json:"access_token"` 100 | RefreshToken string `json:"refresh_token"` 101 | Zone string `json:"zone"` 102 | TPPPassword string `json:"tpp_password"` 103 | TPPUser string `json:"tpp_user"` 104 | TrustBundleFile string `json:"trust_bundle_file"` 105 | Apikey string `json:"apikey"` 106 | CloudURL string `json:"cloud_url"` 107 | } 108 | 109 | func (c venafiSecretEntry) getConnection(zone string) (endpoint.Connector, error) { 110 | cfg, err := c.getConfig(zone, false) 111 | if err == nil { 112 | client, err := vcert.NewClient(cfg) 113 | if err != nil { 114 | return nil, fmt.Errorf("failed to get Venafi issuer client: %s", err) 115 | } else { 116 | return client, nil 117 | } 118 | 119 | } else { 120 | return nil, err 121 | } 122 | 123 | } 124 | 125 | func (c venafiSecretEntry) getConfig(zone string, includeRefreshToken bool) (*vcert.Config, error) { 126 | if zone == "" { 127 | zone = c.Zone 128 | } 129 | 130 | var cfg = &vcert.Config{ 131 | BaseUrl: c.URL, 132 | Zone: zone, 133 | LogVerbose: true, 134 | Credentials: &endpoint.Authentication{}, 135 | } 136 | 137 | if c.URL != "" && c.AccessToken != "" { 138 | cfg.ConnectorType = endpoint.ConnectorTypeTPP 139 | cfg.Credentials.AccessToken = c.AccessToken 140 | if includeRefreshToken { 141 | cfg.Credentials.RefreshToken = c.RefreshToken 142 | } 143 | 144 | } else if c.URL != "" && c.TPPUser != "" && c.TPPPassword != "" { 145 | cfg.ConnectorType = endpoint.ConnectorTypeTPP 146 | cfg.Credentials.User = c.TPPUser 147 | cfg.Credentials.Password = c.TPPPassword 148 | 149 | } else if c.Apikey != "" { 150 | cfg.ConnectorType = endpoint.ConnectorTypeCloud 151 | cfg.Credentials.APIKey = c.Apikey 152 | 153 | } else { 154 | return nil, fmt.Errorf("failed to build config for Venafi conection") 155 | } 156 | 157 | if cfg.ConnectorType == endpoint.ConnectorTypeTPP { 158 | if c.TrustBundleFile != "" { 159 | trustBundle, err := ioutil.ReadFile(c.TrustBundleFile) 160 | if err != nil { 161 | log.Printf("Can`t read trust bundle from file %s: %v\n", c.TrustBundleFile, err) 162 | return nil, err 163 | } 164 | cfg.ConnectionTrust = string(trustBundle) 165 | } 166 | } 167 | return cfg, nil 168 | } 169 | 170 | func (c venafiSecretEntry) getMaskString() string { 171 | return "********" 172 | } 173 | -------------------------------------------------------------------------------- /plugin/pki/venafi_integration_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "encoding/pem" 9 | "github.com/Venafi/vcert/v4/pkg/certificate" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "log" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | func TestAllVenafiIntegrations(t *testing.T) { 21 | /* 22 | Scenario: 23 | Create multiple random roles 24 | Create two policies for TPP 25 | Create two policies for Cloud 26 | Check policy enforecment sync 27 | Check policy default sync 28 | Check certificate import 29 | Check certificate signing 30 | Stress test 31 | */ 32 | rand := randSeq(5) 33 | domain := "vfidev.com" 34 | testRoleName := "test-import" 35 | 36 | policy := copyMap(policyCloudData) 37 | policy2 := copyMap(policyTPPData) 38 | 39 | policy2[policyFieldDefaultsRoles] = "" 40 | policy2[policyFieldEnforcementRoles] = testRoleName + "-2" 41 | policy2[policyFieldImportRoles] = testRoleName + "-1," + testRoleName 42 | 43 | // create the backend 44 | config := logical.TestBackendConfig() 45 | storage := &logical.InmemStorage{} 46 | config.StorageView = storage 47 | 48 | b := Backend(config) 49 | err := b.Setup(context.Background(), config) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | log.Println("create first policy") 55 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 56 | 57 | log.Println("create default role entry") 58 | roleData := map[string]interface{}{ 59 | "allowed_domains": "test.com", 60 | "allow_subdomains": "true", 61 | "max_ttl": "4h", 62 | } 63 | 64 | log.Println("create first role") 65 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 66 | Operation: logical.UpdateOperation, 67 | Path: "roles/" + testRoleName, 68 | Storage: storage, 69 | Data: roleData, 70 | }) 71 | if resp != nil && resp.IsError() { 72 | t.Fatalf("failed to create a role, %#v", resp) 73 | } 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | log.Println("create second role") 79 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 80 | Operation: logical.UpdateOperation, 81 | Path: "roles/" + testRoleName + "-1", 82 | Storage: storage, 83 | Data: roleData, 84 | }) 85 | if resp != nil && resp.IsError() { 86 | t.Fatalf("failed to create a role, %#v", resp) 87 | } 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | log.Println("update first policy") 93 | 94 | policy[policyFieldDefaultsRoles] = testRoleName + "-1," + testRoleName 95 | writePolicy(b, storage, policy, t, defaultVenafiPolicyName) 96 | 97 | log.Println("create third role") 98 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 99 | Operation: logical.UpdateOperation, 100 | Path: "roles/" + testRoleName + "-2", 101 | Storage: storage, 102 | Data: roleData, 103 | }) 104 | if resp != nil && resp.IsError() { 105 | t.Fatalf("failed to create a role, %#v", resp) 106 | } 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | log.Println("write second policy") 112 | 113 | writePolicy(b, storage, policy2, t, defaultVenafiPolicyName+"-1") 114 | 115 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 116 | Operation: logical.ReadOperation, 117 | Path: venafiRolePolicyMapPath, 118 | Storage: storage, 119 | Data: roleData, 120 | }) 121 | if resp != nil && resp.IsError() { 122 | t.Fatalf("failed to read policy map, %#v", resp) 123 | } 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | if resp.Data["policy_map_json"] == "" { 129 | t.Fatalf("There should be data in resp: %s", resp.Data["policy_map_json"]) 130 | } 131 | 132 | var policyMap policyRoleMap 133 | policyMap.Roles = make(map[string]policyTypes) 134 | 135 | err = json.Unmarshal(resp.Data["policy_map_json"].([]byte), &policyMap) 136 | if err != nil { 137 | t.Fatalf("Can not parse policy json data: %s", err) 138 | } 139 | 140 | var want, have string 141 | 142 | want = defaultVenafiPolicyName 143 | have = policyMap.Roles[testRoleName].DefaultsPolicy 144 | if want != have { 145 | t.Fatalf("Policy should be %s but we have %s", want, have) 146 | } 147 | want = "" 148 | have = policyMap.Roles[testRoleName+"-2"].DefaultsPolicy 149 | if want != have { 150 | t.Fatalf("Policy should be %s but we have %s", want, have) 151 | } 152 | want = defaultVenafiPolicyName + "-1" 153 | have = policyMap.Roles[testRoleName+"-2"].EnforcementPolicy 154 | if want != have { 155 | t.Fatalf("Policy should be %s but we have %s", want, have) 156 | } 157 | 158 | want = defaultVenafiPolicyName + "-1" 159 | have = policyMap.Roles[testRoleName].ImportPolicy 160 | if want != have { 161 | t.Fatalf("Policy should be %s but we have %s", want, have) 162 | } 163 | 164 | // generate root 165 | rootData := map[string]interface{}{ 166 | "common_name": "ca.some.domain", 167 | "organization": "Venafi Inc.", 168 | "ou": "Integration", 169 | "locality": "Salt Lake", 170 | "province": "Utah", 171 | "country": "US", 172 | "ttl": "6h", 173 | } 174 | 175 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 176 | Operation: logical.UpdateOperation, 177 | Path: "root/generate/internal", 178 | Storage: storage, 179 | Data: rootData, 180 | }) 181 | if resp != nil && resp.IsError() { 182 | t.Fatalf("failed to generate root, %#v", resp) 183 | } 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | 188 | // config urls 189 | urlsData := map[string]interface{}{ 190 | "issuing_certificates": "http://127.0.0.1:8200/v1/pki/ca", 191 | "crl_distribution_points": "http://127.0.0.1:8200/v1/pki/crl", 192 | } 193 | 194 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 195 | Operation: logical.UpdateOperation, 196 | Path: "config/urls", 197 | Storage: storage, 198 | Data: urlsData, 199 | }) 200 | if resp != nil && resp.IsError() { 201 | t.Fatalf("failed to config urls, %#v", resp) 202 | } 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | var certs_list []string 208 | //Importing certs in multiple roles 209 | var randRoles []string 210 | 211 | for i := 1; i <= 3; i++ { 212 | r := rand + strconv.Itoa(i) + "-role" 213 | randRoles = append(randRoles, r) 214 | } 215 | for _, randRole := range randRoles { 216 | 217 | log.Println("Creating certs for role", randRole) 218 | // create a role entry 219 | roleData := getTPPRoleConfig(domain, 2, 5) 220 | 221 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 222 | Operation: logical.UpdateOperation, 223 | Path: "roles/" + randRole, 224 | Storage: storage, 225 | Data: roleData, 226 | }) 227 | if resp != nil && resp.IsError() { 228 | t.Fatalf("failed to create a role, %#v", resp) 229 | } 230 | if err != nil { 231 | t.Fatal(err) 232 | } 233 | } 234 | 235 | //add created roles to policy 236 | policy2[policyFieldImportRoles] = strings.Join(randRoles, ",") 237 | policy2[policyFieldEnforcementRoles] = strings.Join(randRoles, ",") 238 | policy2[policyFieldDefaultsRoles] = strings.Join(randRoles, ",") 239 | writePolicy(b, storage, policy2, t, defaultVenafiPolicyName) 240 | 241 | log.Println("waiting for roles synchronization") 242 | time.Sleep(30 * time.Second) 243 | 244 | for _, randRole := range randRoles { 245 | //issue some certs 246 | 247 | for j := 1; j < 10; j++ { 248 | randCN := rand + strconv.Itoa(j) + "-import." + domain 249 | certs_list = append(certs_list, randCN) 250 | certData := map[string]interface{}{ 251 | "common_name": randCN, 252 | } 253 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 254 | Operation: logical.UpdateOperation, 255 | Path: "issue/" + randRole, 256 | Storage: storage, 257 | Data: certData, 258 | }) 259 | if resp != nil && resp.IsError() { 260 | t.Fatalf("failed to issue a cert, %#v", resp) 261 | } 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | } 266 | 267 | //list import queue 268 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 269 | Operation: logical.ListOperation, 270 | Path: "import-queue/", 271 | Storage: storage, 272 | }) 273 | if resp != nil && resp.IsError() { 274 | t.Fatalf("failed to list certs, %#v", resp) 275 | } 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | keys := resp.Data["keys"] 280 | t.Logf("Import queue list is:\n %v", keys) 281 | 282 | } 283 | 284 | log.Println("Waiting for certs to import") 285 | time.Sleep(45 * time.Second) 286 | //After creating all certificates we need to check that they exist in TPP 287 | log.Println("Trying check all certificates from list", certs_list) 288 | for _, singleCN := range certs_list { 289 | //retrieve imported certificate 290 | //res.Certificates[0].CertificateRequestId != "\\VED\\Policy\\devops\\vcert\\renx3.venafi.example.com" 291 | log.Println("Trying to retrieve requested certificate", singleCN) 292 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 293 | 294 | req := &certificate.Request{} 295 | req.PickupID = "\\VED\\Policy\\" + os.Getenv("TPP_ZONE") + "\\" + singleCN 296 | req.ChainOption = certificate.ChainOptionIgnore 297 | //req.Thumbprint = "111111" 298 | 299 | cl := getTPPConnection(t) 300 | pcc, err := cl.RetrieveCertificate(req) 301 | if err != nil { 302 | t.Fatalf("could not retrieve certificate using requestId %s: %s", req.PickupID, err) 303 | } 304 | //t.Logf("Got certificate\n:%s",pp(pcc.Certificate)) 305 | block, _ := pem.Decode([]byte(pcc.Certificate)) 306 | cert, err := x509.ParseCertificate(block.Bytes) 307 | if err != nil { 308 | t.Fatalf("Error parsing cert: %s", err) 309 | } 310 | if cert.Subject.CommonName != singleCN { 311 | t.Fatalf("Incorrect subject common name: expected %v, got %v", cert.Subject.CommonName, singleCN) 312 | } else { 313 | t.Logf("Subject common name: expected %v, got %v", cert.Subject.CommonName, singleCN) 314 | } 315 | } 316 | 317 | } 318 | -------------------------------------------------------------------------------- /plugin/pki/venafi_util.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/vault/api" 6 | logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" 7 | "github.com/hashicorp/vault/sdk/logical" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | const ( 13 | logPrefixVenafiImport = "VENAFI_IMPORT: " 14 | logPrefixVenafiPolicyEnforcement = "VENAFI_POLICY_ENFORCEMENT: " 15 | logPrefixVenafiRoleyDefaults = "VENAFI_ROLE_DEFAULTS: " 16 | logPrefixVenafiScheduler = "VENAFI_SCHEDULER: " 17 | logPrefixVenafiSecret = "VENAFI_SECRET: " 18 | ) 19 | 20 | const msg_denied_by_policy = "certificate issue should be denied by policy, %#v" 21 | const wrong_csr = `-----BEGIN CERTIFICATE REQUEST----- 22 | MIIFSjCCAzICAQAwgaQxCzAJBgNVBAYTAldDMQ0wCwYDVQQIDARVdGFoMRcwFQYD 23 | VQQHDA5Xcm9uZyBMb2NhbGl0eTESMBAGA1UECgwJV3JvbmcgT3JnMRMwEQYDVQQL 24 | DApXcm9uZyBVbml0MR4wHAYJKoZIhvcNAQkBFg9lbWFpbEB3cm9uZy5jb20xJDAi 25 | BgNVBAMMG3Rlc3QtY3NyLTMyMzEzMTMxLndyb25nLmNvbTCCAiIwDQYJKoZIhvcN 26 | AQEBBQADggIPADCCAgoCggIBALuwFXjQk0BY2z35uS7rp+tpznZS2oyY1HZC2ZXb 27 | w8vIp9UOoYa9+919Gl+28Zr1K0ClW/4VY4h/p93HxuaJ0Emg9PlF2qHvwbR+uY1/ 28 | nY+0NK1deNy4xB1D0RQ9zMULzYhFXRE0ryrcEFVmad75tPQcvn+s61G4itY28uVu 29 | d+7IKkMBvf1t2516dwrD9mMP5lUZQaLgeMvBWh/dDt94Ag/MIcHo7ceOTuMe10II 30 | tqzBz6/qcCYt2glKoJFsmDomR3x/29451nF7orIFafg3dXum8LQy26XG9j8fcUUz 31 | DxQHPp40k8Oc2pHuqKo7cCu9Oql4P+F9EGng1dJwMmVOQbuUj0OdqkVHwygabx/w 32 | 3WfBZqdFYbkvOFYJiMC3b+7GsWPvqf9/eA+l4Vnq/8LwUQbKdt23k7MDzw75uqO/ 33 | sntkBw9XgQeny6p4s7b0lLiFmyKwiKScws/dwdQ5s6y+H7u6lQNfsicDitTPMP20 34 | EQ3nnjM9ENfEhDl7Muhyb+DAb7Vs1rARc6BOclwxYUDMNOErRBqedRCrj1nchOxy 35 | HM4Nz/Csn+PhHyoOFuCGdc0lrvegjNF/inVYlicyzqH6WUlnNUg4k2nrhPJwUo79 36 | FKsJ/UEsNvrxSr5L7kX6l/F6DKLHXX5kVEFD/83mTTOKw8AWTw96ASEX7J3AmY7C 37 | 8f/PAgMBAAGgYDBeBgkqhkiG9w0BCQ4xUTBPME0GA1UdEQRGMESCIGFsdDEtdGVz 38 | dC1jc3ItMzIzMTMxMzEud3JvbmcuY29tgiBhbHQyLXRlc3QtY3NyLTMyMzEzMTMx 39 | Lndyb25nLmNvbTANBgkqhkiG9w0BAQsFAAOCAgEAnVM3zi+Zeknpg3R/XTyVYdpX 40 | 31EA0aDg7SVm6iSIyD1iITPJQ1fGDY3/GaRUdD5TLzmyOohFS4dj2FV2zRi9BzfU 41 | xqgy5zONGtXxzefiDCicc1aP2eduiQ/Gg1NSMopOYK5ppKfPHqSp+k4O3oYpn3oS 42 | lkox7dez84gw3TdA68EFizE7JbwRV6CCit4EY1ZHM/tzhBogmr9yDxdNlNd1zzTL 43 | tWMRU2vOVubbGCScapJehTIc+aOchNGrxDazmRwVuVFIE4Mw+9ALJJ3rJvGqZ6XF 44 | 5Fk0TVSuOTto4m0WHUAh+VeyfV4ZZEHwRtCv0y7e7mp7ZHiFKHsGUT2Ll7Ssp2o+ 45 | gdvwXrPsWhkbvuO9CQuh75BRCDqgBO4eVzIZ5DBur5/H8Nl6y9M44Mh2LRR/FYr5 46 | pSyelv3jpGOuIq4obNch2yYLDwftEm7KuQI4YUpsZFZXeMUmvKop1rVBqLejcotK 47 | NwnkGHoG3xeCk3x01af09B7YJfMnV/HCh3k5gf8XGgdpfNg4MjsrYRdFQ/fNTiv1 48 | b7/jDBHlXox4Nxptg2aASDJR3iFfMdBju548SAeD984lq/lXcjII2yL6h8VkCQpd 49 | kBLCbOylNnLu/CGd907fpBpWQ6rptGLnVEAs2ab02mcD0Ul4iVA4lXoLlj39JGyB 50 | lICcWSA1Gqz34IAXJco= 51 | -----END CERTIFICATE REQUEST----- 52 | ` 53 | const allowed_csr = `-----BEGIN CERTIFICATE REQUEST----- 54 | MIIFTDCCAzQCAQAwgaQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRIwEAYD 55 | VQQHDAlTYWx0IExha2UxFDASBgNVBAoMC1ZlbmFmaSBJbmMuMRQwEgYDVQQLDAtJ 56 | bnRlZ3JhdGlvbjEfMB0GCSqGSIb3DQEJARYQZW1haWxAdmZpZGV2LmNvbTElMCMG 57 | A1UEAwwcdGVzdC1jc3ItMzIzMTMxMzEudmZpZGV2LmNvbTCCAiIwDQYJKoZIhvcN 58 | AQEBBQADggIPADCCAgoCggIBALGdS+40Lj1qWDMl9+hKiUtn2/PJzRA0yGSf8xAp 59 | 3HAxm6iXWTMkHBmWdm22FhatXt+6qSb+k2el7jfHEyVesMaKqw91C3Ht9LVuXLK4 60 | xdb2QlKz/AaBMbh9kVUD//NrJM0VbNxflDMG8EWEpZeE9qUDMQQ8eB1fwBf824TP 61 | XskiIqzo5HkRWBHmxvvKL0NWCPG4gy33yTyNwH2MBA5xMb+584/TEQkEPQDl14gj 62 | 1uR2B1Ndd8V0Yv/UCu1PjM3Nn2CrcN2/dQLTSNoMhLt/woxdxDiUOzumUPJ1vBVg 63 | fEjGA+EIq/IkDgSNz4h5dUhdnEiMxe2yIHNhrOeomIaTbiRPGaMV/0JLhNQin6ug 64 | y0ws3Tk8MwM0s+FLka62LFea7WbT5qTlkhvnJZdlbPD8j5h0+OamLmhB5jvTlJUW 65 | IPpC8fQx4wjYq0xX0R9FMd1YQInoEVwH6Hd57iv+aqGD90UkcfXKj8BvDD8WdRAI 66 | l4IAKHxLUtNRFAU+hv99kwX8KRIkHLiVJg6AhRhvSm84ClYi4OPEEvaw70gNwOAO 67 | JkpbOttmSALLVoVn30bdayW0m7UAfiWtI3Ax+okthdELfdHrPPZK7d0SCB3VCeGp 68 | ydQEjHwwttqEFFnkcpPMMZez7XW6MwJi1mneXvWoRzhX+4gt7OkahHEL6Lhj14nY 69 | d1rjAgMBAAGgYjBgBgkqhkiG9w0BCQ4xUzBRME8GA1UdEQRIMEaCIWFsdDEtdGVz 70 | dC1jc3ItMzIzMTMxMzEudmZpZGV2LmNvbYIhYWx0Mi10ZXN0LWNzci0zMjMxMzEz 71 | MS52ZmlkZXYuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQAwt3Jc78Z1j7fjxQrsBl8m 72 | ofuqwjqbbtLPu9uYbW9ZHdKwq7zpKShT942UZckzPiQKxy8bXVQ1MDrEzpfJKOpp 73 | 1tAqvn9pN3B3qxYKZOjzEmZgdAT57NiZSziN2vSY89aF28Ppz8ZUOFsiOvwuFBvQ 74 | LLQopJ6mJEvMlv8+7CCQzumeIVRnxBjqqXnfJCBW9Dwcf1pAnsQv7RFf4XwU86dY 75 | 8GyLtpsq4wOJqzjbReCSjIJydqE/12QLOgzpT8a4Z1Srh6ZHfxWzIiAUQvrE4hHM 76 | Exs0kkcJkUutlwMeaPACZkg3Tigqc72Y6YUeruBhEST5ypRrZGJJGHJLqpEHSPBb 77 | w9B20cUctGxUQ4h1ogNCrz5XWjM+Khv+k5rkPhTQo0OnVglTkSzWV9FLufgUyu3E 78 | O/KCByFbOckP56UxGFvVReJREPqa3Ib3QvTgIi810fe5SSmunpRCYBnKeqq4IVi5 79 | 4kXQNuHvV2wntJyIfEWZub7eXnHqP6OBndbo/0y26wYwKFRAgZDshIyPQUe01YIF 80 | Gr6H0CHR3dU7Py0S49pAA+Jc93up5w056w5zhmjiv5c2N7m44VYJuxpwqfwtFxFF 81 | WNHMdQt0cab08o2FGdJ3gtN4Fp1Fq+BRkgnST3ZISozd6nZXLuejXWjC+jNDvU/e 82 | H2IE+vUez839sw9RlLcjgw== 83 | -----END CERTIFICATE REQUEST----- 84 | ` 85 | 86 | const allowed_empty_csr = `-----BEGIN CERTIFICATE REQUEST----- 87 | MIIEyTCCArECAQAwgYMxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRIwEAYD 88 | VQQHDAlTYWx0IExha2UxFDASBgNVBAoMC1ZlbmFmaSBJbmMuMRQwEgYDVQQLDAtJ 89 | bnRlZ3JhdGlvbjElMCMGA1UEAwwcdGVzdC1jc3ItMzIzMTMxMzEudmZpZGV2LmNv 90 | bTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALsSuhL/AGryMUO+jrtM 91 | xYfp2vUlgIKu/uGfm+4ILGrtWlH/i5+aoT3I+blUPskDDMLhvG4lD4xT9urxcnJj 92 | 2sO8hOPnSsTvMnAOLgi8OW2JMeeA74BsPi2lxXvW/392EAxPjxuMKULI0gm60vBi 93 | yFtS6wRinvEUDkSy8r8Z0a0zsqsRKt9VMgJy2tEmRYMT0xkfC8GkARhgKNCe7kHJ 94 | OjCS6MZgJSHekZBxKsLjblQFrHSCT0SrWJDLlHIwd9CL0uVkqe9UMfJ8Nm7WowXe 95 | BrDUHNSOUjZo8jjqCSnu9vVw/MR4paMssSKcyXSKQcsUJQBfoWBMGTCUZolA86TM 96 | U7DMxPorXm94ZfHiOa6qS5A20Z7VUvCp8BR3RF0b/5ntJwGULbMg5QBAZ6sF+rnm 97 | BN5xAyrGHYck2TqphyZRAFRs/yuVdQx+3wHykAqzzX7cYlzI3EhT/3yQu5VZNclq 98 | wxvCsT197s2VB8tcPxvAlBddKkLVY+hp4U5dxEDLxOf5+oryGx+6nOZdWlYKxxNZ 99 | 7P83oOjp95+UCVeDGDwI7+8y7OwK8AF82HqbeDm6dliKNQ4tnN1ddzWabXfuzuYl 100 | OmdTiKDNR/gHUq2HaxekKHs39ToXyJg65+g3baEX5JM6KKmrXL2N8s6YiMMaF13m 101 | 3SPa3pzGKNNLYcqdkEjRjlKBAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAgEAJ4RK 102 | 9RrnI7AblRCYG5Rnyu/YvVkSu7gnp+04DeECntxv9FyP99aPgGPxlMzH11QFUwKZ 103 | Kn4XLz8559JGA8umcWT559hQ4XFpYoyzEvnf/vdA9NAmSr0ssMsoZ1DjR4l0rR2m 104 | Y+doP4CAqRh1rC6JDTMvo/WxwJRImgrnziyOwZba7mwDRNVIXWa70cFPfgb3fKro 105 | egPkp/Hqvho0Rvu3m3o5Y35UxKiMylZUX3pHdpKXVG2wxj0FgeOepd4cFSHrF85q 106 | uPhc12CDvv71wtxMcL8mmWizjpuGGBvDx0Tz8uJmaumNkIwZ+GGhqBsAPJI/YCy6 107 | 44WYs9vRDCjHnIXIazJTc3kFwaDOJF3btCYQ6dG1dHh8lRLnfkYLOtKlJ2gbrUqB 108 | s44QoRhU5ZYUD1+8TYNWQtgceGjCTACsbxH4JKOG38NT4C/mv3ZsEC1yTfjxWRDH 109 | CqLGi3SbYFiUEk0WRWAbwe80HtcAVCFCa2G3C/FGS/qCiFIbE9op5Ab3NDWJGdSI 110 | gT230FFz4jsyW4395IiZ8UOoLXxBmnL382+hdB08aEdm4j/ZFeeButG0qb3XhUu3 111 | /atUO1Boht8DNna1DH/1uLW0ovAAKgX+v3LTi/vadErW/X3S7P/ZnbLY5pA7nEBg 112 | bOcvXbCN3l5HIY76e+6FbLGGCvNKcgNpSAAPYJg= 113 | -----END CERTIFICATE REQUEST----- 114 | ` 115 | 116 | var venafiTestTPPConfigAllAllow = map[string]interface{}{ 117 | "tpp_url": os.Getenv("TPP_URL"), 118 | "tpp_user": os.Getenv("TPP_USER"), 119 | "tpp_password": os.Getenv("TPP_PASSWORD"), 120 | "zone": os.Getenv("TPP_ZONE"), 121 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 122 | "auto_refresh_interval": 1, 123 | "venafi_secret": venafiSecretDefaultName + "tpp", 124 | } 125 | 126 | var venafiTestTPPConfigNoRefresh = map[string]interface{}{ 127 | "tpp_url": os.Getenv("TPP_URL"), 128 | "tpp_user": os.Getenv("TPP_USER"), 129 | "tpp_password": os.Getenv("TPP_PASSWORD"), 130 | "zone": os.Getenv("TPP_ZONE"), 131 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 132 | "auto_refresh_interval": 0, 133 | "venafi_secret": venafiSecretDefaultName + "tpp_noRefresh", 134 | } 135 | 136 | var venafiTestConfigBadData = map[string]interface{}{ 137 | "cloud_url": os.Getenv("CLOUD_URL"), 138 | "apikey": os.Getenv("CLOUD_APIKEY"), 139 | "zone": os.Getenv("CLOUD_ZONE_RESTRICTED"), 140 | "venafi_secret": venafiSecretDefaultName + "badData", 141 | } 142 | 143 | var venafiTestTPPConfigRestricted = map[string]interface{}{ 144 | "tpp_url": os.Getenv("TPP_URL"), 145 | "tpp_user": os.Getenv("TPP_USER"), 146 | "tpp_password": os.Getenv("TPP_PASSWORD"), 147 | "zone": os.Getenv("TPP_ZONE_RESTRICTED"), 148 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 149 | "auto_refresh_interval": 1, 150 | "venafi_secret": venafiSecretDefaultName + "tpp_restricted", 151 | } 152 | 153 | var venafiTestCloudConfigRestricted = map[string]interface{}{ 154 | "cloud_url": os.Getenv("CLOUD_URL"), 155 | "apikey": os.Getenv("CLOUD_APIKEY"), 156 | "zone": os.Getenv("CLOUD_ZONE_RESTRICTED"), 157 | "auto_refresh_interval": 1, 158 | "venafi_secret": venafiSecretDefaultName + "cloud_restricted", 159 | } 160 | 161 | var venafiTestTokenConfigRestricted = map[string]interface{}{ 162 | "url": os.Getenv("TPP_TOKEN_URL"), 163 | "access_token": os.Getenv("TPP_ACCESS_TOKEN"), 164 | "zone": os.Getenv("TPP_ZONE_RESTRICTED"), 165 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 166 | "auto_refresh_interval": 1, 167 | "venafi_secret": venafiSecretDefaultName + "token_restricted", 168 | } 169 | 170 | var venafiTestCloudConfigAllAllow = map[string]interface{}{ 171 | "cloud_url": os.Getenv("CLOUD_URL"), 172 | "apikey": os.Getenv("CLOUD_APIKEY"), 173 | "zone": os.Getenv("CLOUD_ZONE"), 174 | "auto_refresh_interval": 1, 175 | "venafi_secret": venafiSecretDefaultName + "cloud", 176 | } 177 | 178 | var venafiTestTPPConfigImportOnlyNonCompliant = map[string]interface{}{ 179 | "tpp_url": os.Getenv("TPP_URL"), 180 | "tpp_user": os.Getenv("TPP_USER"), 181 | "tpp_password": os.Getenv("TPP_PASSWORD"), 182 | "zone": os.Getenv("TPP_ZONE_RESTRICTED"), 183 | "trust_bundle_file": os.Getenv("TRUST_BUNDLE"), 184 | "import_only_non_compliant": true, 185 | "auto_refresh_interval": 1, 186 | "venafi_secret": venafiSecretDefaultName + "tpp", 187 | } 188 | var createVenafiSecretStep = logicaltest.TestStep{ 189 | Operation: logical.UpdateOperation, 190 | Path: venafiSecretPath + venafiTestTPPConfigAllAllow["venafi_secret"].(string), 191 | Data: venafiTestTPPConfigAllAllow, 192 | } 193 | 194 | var venafiTPPCreateSimplePolicyStep = logicaltest.TestStep{ 195 | Operation: logical.UpdateOperation, 196 | Path: venafiPolicyPath + defaultVenafiPolicyName, 197 | Data: venafiTestTPPConfigAllAllow, 198 | } 199 | var venafiCloudCreateSimplePolicyStep = logicaltest.TestStep{ 200 | Operation: logical.UpdateOperation, 201 | Path: venafiPolicyPath + defaultVenafiPolicyName, 202 | Data: venafiTestCloudConfigAllAllow, 203 | } 204 | 205 | func makeVenafiCloudConfig() (domain string, policyData map[string]interface{}) { 206 | domain = "vfidev.com" 207 | policyData = copyMap(venafiTestCloudConfigRestricted) 208 | return 209 | } 210 | 211 | func makeVenafiTPPConfig() (domain string, policyData map[string]interface{}) { 212 | domain = "vfidev.com" 213 | policyData = copyMap(venafiTestTPPConfigRestricted) 214 | return 215 | } 216 | 217 | func makeVenafiTokenConfig() (domain string, policyData map[string]interface{}) { 218 | domain = "vfidev.com" 219 | policyData = copyMap(venafiTestTokenConfigRestricted) 220 | return 221 | } 222 | 223 | func writeVenafiSecret(b *backend, storage logical.Storage, secretData map[string]interface{}, t *testing.T, venafiSecretName string) *logical.Response { 224 | t.Log("Writing Venafi secret configuration") 225 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 226 | Operation: logical.UpdateOperation, 227 | Path: venafiSecretPath + venafiSecretName, 228 | Storage: storage, 229 | Data: secretData, 230 | }) 231 | if resp != nil && resp.IsError() { 232 | t.Fatalf("failed to configure venafi secret, %#v", resp) 233 | } 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | 238 | return resp 239 | } 240 | 241 | func writePolicy(b *backend, storage logical.Storage, policyData map[string]interface{}, t *testing.T, policyName string) *logical.Response { 242 | 243 | secretName := policyData["venafi_secret"].(string) 244 | if secretName == "" { 245 | t.Fatalf("failed to read Venafi Secret on policy %s. Looks like its empty", policyName) 246 | } 247 | 248 | writeVenafiSecret(b, storage, policyData, t, secretName) 249 | 250 | t.Log("Writing Venafi policy configuration") 251 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 252 | Operation: logical.UpdateOperation, 253 | Path: venafiPolicyPath + policyName, 254 | Storage: storage, 255 | Data: policyData, 256 | }) 257 | if resp != nil && resp.IsError() { 258 | t.Fatalf("failed to configure venafi policy, %#v", resp) 259 | } 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | if resp == nil { 264 | t.Fatalf("after write policy should be on output, but response is nil: %#v", resp) 265 | } 266 | return resp 267 | } 268 | 269 | func writePolicyToClient(mountPoint string, client *api.Client, t *testing.T) { 270 | venafiSecretName := venafiTestTPPConfigAllAllow["venafi_secret"].(string) 271 | _, err := client.Logical().Write(mountPoint+"/"+venafiSecretPath+venafiSecretName, venafiTestTPPConfigAllAllow) 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | 276 | _, err = client.Logical().Write(mountPoint+"/"+venafiPolicyPath+defaultVenafiPolicyName, venafiTestTPPConfigAllAllow) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | } 281 | 282 | func checkRoleEntry(t *testing.T, haveRoleEntryData roleEntry, wantRoleEntryData roleEntry) { 283 | var want string 284 | var have string 285 | 286 | want = wantRoleEntryData.OU[0] 287 | have = haveRoleEntryData.OU[0] 288 | if have != want { 289 | t.Fatalf("%s doesn't match %s", have, want) 290 | } 291 | 292 | want = wantRoleEntryData.Organization[0] 293 | have = haveRoleEntryData.Organization[0] 294 | if have != want { 295 | t.Fatalf("%s doesn't match %s", have, want) 296 | } 297 | 298 | want = wantRoleEntryData.Country[0] 299 | have = haveRoleEntryData.Country[0] 300 | if have != want { 301 | t.Fatalf("%s doesn't match %s", have, want) 302 | } 303 | 304 | want = wantRoleEntryData.Locality[0] 305 | have = haveRoleEntryData.Locality[0] 306 | if have != want { 307 | t.Fatalf("%s doesn't match %s", have, want) 308 | } 309 | 310 | want = wantRoleEntryData.Province[0] 311 | have = haveRoleEntryData.Province[0] 312 | if have != want { 313 | t.Fatalf("%s doesn't match %s", have, want) 314 | } 315 | 316 | if !testEqStrginSlice(wantRoleEntryData.AllowedDomains, haveRoleEntryData.AllowedDomains) { 317 | t.Fatalf("%s doesn't match %s", wantRoleEntryData.AllowedDomains, haveRoleEntryData.AllowedDomains) 318 | } 319 | 320 | want = wantRoleEntryData.KeyUsage[0] 321 | have = haveRoleEntryData.KeyUsage[0] 322 | if have != want { 323 | t.Fatalf("%s doesn't match %s", have, want) 324 | } 325 | } 326 | 327 | func testEqStrginSlice(a, b []string) bool { 328 | 329 | // If one is nil, the other must also be nil. 330 | if (a == nil) != (b == nil) { 331 | return false 332 | } 333 | 334 | if len(a) != len(b) { 335 | return false 336 | } 337 | 338 | for i := range a { 339 | if a[i] != b[i] { 340 | return false 341 | } 342 | } 343 | 344 | return true 345 | } 346 | 347 | func sliceContains(slice []string, item string) bool { 348 | set := make(map[string]struct{}, len(slice)) 349 | for _, s := range slice { 350 | set[s] = struct{}{} 351 | } 352 | 353 | _, ok := set[item] 354 | return ok 355 | } 356 | 357 | func copyMap(m map[string]interface{}) map[string]interface{} { 358 | cp := make(map[string]interface{}) 359 | for k, v := range m { 360 | vm, ok := v.(map[string]interface{}) 361 | if ok { 362 | cp[k] = copyMap(vm) 363 | } else { 364 | cp[k] = v 365 | } 366 | } 367 | 368 | return cp 369 | } 370 | -------------------------------------------------------------------------------- /scripts/allowed_csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 4096 3 | prompt = no 4 | default_md = sha256 5 | req_extensions = req_ext 6 | distinguished_name = dn 7 | 8 | [ dn ] 9 | C=US 10 | ST=Utah 11 | L=Salt Lake 12 | O=Venafi Inc. 13 | OU=Integration 14 | emailAddress=email@vfidev.com 15 | CN = test-csr-32313131.vfidev.com 16 | 17 | [ req_ext ] 18 | subjectAltName = @alt_names 19 | 20 | [ alt_names ] 21 | DNS.1 = alt1-test-csr-32313131.vfidev.com 22 | DNS.2 = alt2-test-csr-32313131.vfidev.com 23 | -------------------------------------------------------------------------------- /scripts/allowed_empty_csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 4096 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = dn 6 | 7 | [ dn ] 8 | C=US 9 | ST=Utah 10 | L=Salt Lake 11 | O=Venafi Inc. 12 | OU=Integration 13 | CN = test-csr-32313131.vfidev.com 14 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | PLUGIN_NAME=$1 5 | PLUGIN_DIR=$2 6 | DIST_DIR=$3 7 | BUILD_MODE=$4 8 | VERSION=$5 9 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 10 | 11 | if [ ${BUILD_MODE} == "strict" ]; then 12 | echo "Changing venafiPolicyDenyAll to true" 13 | sed -i 's/var venafiPolicyDenyAll =.*/var venafiPolicyDenyAll = true/' plugin/pki/vcert.go 14 | elif [ ${BUILD_MODE} == "optional" ]; then 15 | echo "Changing venafiPolicyDenyAll to false" 16 | sed -i 's/var venafiPolicyDenyAll =.*/var venafiPolicyDenyAll = false/' plugin/pki/vcert.go 17 | else 18 | echo "Can't determine build mode" 19 | exit 1 20 | fi 21 | 22 | mkdir -p ${CURRENT_DIR}/../${DIST_DIR} 23 | 24 | for os in linux darwin windows; do 25 | for arch in 386 amd64; do 26 | # 32-bit macOS build is now retired since support was dropped in Go 1.15 27 | if [ "${os}" == "darwin" ] && [ "${arch}" == "386" ]; then continue; fi 28 | 29 | case "${arch}" in 30 | 386) EXT="86" ;; 31 | *) EXT="" ;; 32 | esac 33 | binary_name="${PLUGIN_DIR}/${os}${EXT}/${PLUGIN_NAME}" 34 | archive_name="${CURRENT_DIR}/../${DIST_DIR}/${PLUGIN_NAME}_${VERSION}_${os}${EXT}_${BUILD_MODE}" 35 | 36 | if [ ${BUILD_MODE} != "strict" ]; then 37 | binary_name="${binary_name}_${BUILD_MODE}" 38 | fi 39 | 40 | if [ "${os}" == "windows" ]; then 41 | binary_name="${binary_name}.exe" 42 | fi 43 | 44 | echo "Building plugin binary ${binary_name} for ${os}-${arch}" 45 | env CGO_ENABLED=0 GOOS=${os} GOARCH=${arch} go build -ldflags '-s -w -extldflags "-static"' -a -o ${binary_name} || exit 1 46 | chmod +x ${binary_name} 47 | echo "Archiving binary into ${archive_name}.zip" 48 | SHA256=$(sha256sum ${binary_name}| head -c 64) 49 | echo "${SHA256}" > ${binary_name}.SHA256SUM 50 | zip -j "${archive_name}.zip" "${binary_name}" "${binary_name}.SHA256SUM" 51 | done 52 | done 53 | echo "Changing venafiPolicyDenyAll to default value(true)" 54 | sed -i 's/var venafiPolicyDenyAll =.*/var venafiPolicyDenyAll = true/' plugin/pki/vcert.go -------------------------------------------------------------------------------- /scripts/gen_test_csr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | openssl req -new -newkey rsa:4096 -nodes -out allowed.csr -keyout allowed.key -config allowed_csr.conf 3 | openssl req -new -newkey rsa:4096 -nodes -out allowed_empty.csr -keyout allowed_empty.key -config allowed_empty_csr.conf 4 | openssl req -new -newkey rsa:4096 -nodes -out wrong.csr -keyout wrong.key -config wrong_csr.conf 5 | -------------------------------------------------------------------------------- /scripts/gofmtcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check gofmt 4 | echo "==> Checking that code complies with gofmt requirements..." 5 | gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) 6 | if [[ -n ${gofmt_files} ]]; then 7 | echo 'gofmt needs running on the following files:' 8 | echo "${gofmt_files}" 9 | echo "You can use the command: \`make fmt\` to reformat code." 10 | exit 1 11 | fi 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /scripts/vault-config-with-consul.hcl: -------------------------------------------------------------------------------- 1 | backend "consul" { 2 | address = "consul:8500" 3 | advertise_addr = "http://127.0.0.1:8200" 4 | path = "vault" 5 | scheme = "http" 6 | } 7 | 8 | listener "tcp" { 9 | address = "0.0.0.0:8200" 10 | tls_disable = 1 11 | } 12 | 13 | disable_mlock = true 14 | ui = true 15 | plugin_directory = "/vault_plugin/" 16 | -------------------------------------------------------------------------------- /scripts/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### adopted from https://github.com/ucalgary/wait-for-it 4 | ### so it works for the alpine image the official Vault image derives from 5 | 6 | ########################################################## 7 | # # 8 | # waits until a given TCP host:port is available # 9 | # when available runs a provided command with args # 10 | # # 11 | ########################################################## 12 | 13 | cmdname=$(basename $0) 14 | 15 | echoerr() { if [ $QUIET -ne 1 ]; then echo "$@" 1>&2; fi } 16 | 17 | usage() 18 | { 19 | cat << USAGE >&2 20 | Usage: 21 | $cmdname host:port [-s] [-t timeout] [-- command args] 22 | -h HOST Host or IP under test 23 | -p PORT TCP port under test 24 | -s Only execute subcommand if the test succeeds 25 | -q Do not output any status messages 26 | -t TIMEOUT Timeout in seconds, zero for no timeout 27 | -- COMMAND ARGS Execute command with args after the test finishes 28 | USAGE 29 | exit 1 30 | } 31 | 32 | wait_for() 33 | { 34 | if [ $TIMEOUT -gt 0 ]; then 35 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 36 | else 37 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 38 | fi 39 | start_ts=$(date +%s) 40 | while : 41 | do 42 | if [ $ISBUSY -eq 1 ]; then 43 | nc -z $HOST $PORT 44 | result=$? 45 | else 46 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 47 | result=$? 48 | fi 49 | if [ $result -eq 0 ]; then 50 | end_ts=$(date +%s) 51 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 52 | break 53 | fi 54 | sleep 1 55 | done 56 | return $result 57 | } 58 | 59 | wait_for_wrapper() 60 | { 61 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 62 | if [ $QUIET -eq 1 ]; then 63 | timeout $BUSYTIMEFLAG $TIMEOUT $0 -q -c -h $HOST -p $PORT -t $TIMEOUT & 64 | else 65 | timeout $BUSYTIMEFLAG $TIMEOUT $0 -c -h $HOST -p $PORT -t $TIMEOUT & 66 | fi 67 | PID=$! 68 | trap "kill -INT -$PID" INT 69 | wait $PID 70 | RESULT=$? 71 | if [[ $RESULT -ne 0 ]]; then 72 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 73 | fi 74 | return $RESULT 75 | } 76 | 77 | while getopts csqt:h:p: OPT; do 78 | case "$OPT" in 79 | c) 80 | CHILD=1 81 | ;; 82 | s) 83 | STRICT=1 84 | ;; 85 | q) 86 | QUIET=1 87 | ;; 88 | t) 89 | TIMEOUT=$OPTARG 90 | ;; 91 | h) 92 | HOST=$OPTARG 93 | ;; 94 | p) 95 | PORT=$OPTARG 96 | ;; 97 | esac 98 | done 99 | 100 | shift `expr $OPTIND - 1` 101 | 102 | CLI="$@" 103 | TIMEOUT=${TIMEOUT:-15} 104 | STRICT=${STRICT:-0} 105 | CHILD=${CHILD:-0} 106 | QUIET=${QUIET:-0} 107 | 108 | if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then 109 | echoerr "Error: you need to provide a host and port to test." 110 | usage 111 | fi 112 | 113 | # check to see if timeout is from busybox 114 | TIMEOUT_PATH=$(realpath $(which timeout)) 115 | BUSYBOX="busybox" 116 | if test "${TIMEOUT_PATH#*$BUSYBOX}" != "$BUSYBOX"; then 117 | ISBUSY=1 118 | BUSYTIMEFLAG="-t" 119 | else 120 | ISBUSY=0 121 | BUSYTIMEFLAG="" 122 | fi 123 | 124 | if [ $CHILD -gt 0 ]; then 125 | wait_for 126 | RESULT=$? 127 | exit $RESULT 128 | else 129 | if [ $TIMEOUT -gt 0 ]; then 130 | wait_for_wrapper 131 | RESULT=$? 132 | else 133 | wait_for 134 | RESULT=$? 135 | fi 136 | fi 137 | 138 | if [ ! -z "$CLI" ]; then 139 | if [ $RESULT -ne 0 ] && [ $STRICT -eq 1 ]; then 140 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 141 | exit $RESULT 142 | fi 143 | exec $CLI 144 | else 145 | exit $RESULT 146 | fi 147 | -------------------------------------------------------------------------------- /scripts/wrong_csr.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 4096 3 | prompt = no 4 | default_md = sha256 5 | req_extensions = req_ext 6 | distinguished_name = dn 7 | 8 | [ dn ] 9 | C=WC 10 | ST=Utah 11 | L=Wrong Locality 12 | O=Wrong Org 13 | OU=Wrong Unit 14 | emailAddress=email@wrong.com 15 | CN = test-csr-32313131.wrong.com 16 | 17 | [ req_ext ] 18 | subjectAltName = @alt_names 19 | 20 | [ alt_names ] 21 | DNS.1 = alt1-test-csr-32313131.wrong.com 22 | DNS.2 = alt2-test-csr-32313131.wrong.com 23 | -------------------------------------------------------------------------------- /vault-config.hcl: -------------------------------------------------------------------------------- 1 | plugin_directory = "pkg/bin" 2 | --------------------------------------------------------------------------------