Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
Please login to access the resource
openSUSE:Factory:Rebuild
vault-sync
vault-sync-0.9.2.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File vault-sync-0.9.2.obscpio of Package vault-sync
07070100000000000081A4000000000000000000000001665A0E0D00000010000000000000000000000000000000000000001F00000000vault-sync-0.9.2/.dockerignoretarget/ docker/ 07070100000001000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001900000000vault-sync-0.9.2/.github07070100000002000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000002300000000vault-sync-0.9.2/.github/workflows07070100000003000081A4000000000000000000000001665A0E0D00001550000000000000000000000000000000000000002A00000000vault-sync-0.9.2/.github/workflows/ci.ymlname: CI on: push: branches: - main tags: - '*' pull_request: branches: - main env: CARGO_TERM_COLOR: always VAULT_ADDR: http://127.0.0.1:8200 jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose - name: Install Vault run: | sudo apt-get update -y sudo apt-get install -y gpg wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg >/dev/null gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update -y sudo apt-get install -y vault - name: Run local test run: ./scripts/test.sh - name: Run Vault in background run: | vault server -dev -dev-root-token-id=unsafe-root-token & # Make sure Vault is running sleep 1 while ! vault token lookup; do sleep 1; done - name: Enable AppRole auth method run: | vault auth enable approle - name: Enable secrets engine at the custom path run: | vault secrets enable -path=secret2 kv-v2 - name: Make sure both standard and custom secrets engine are present run: | vault secrets list vault secrets list | grep -qE '^secret/\s+kv' vault secrets list | grep -qE '^secret2/\s+kv' - name: Create Vault policy for reader run: | cat <<EOF | vault policy write vault-sync-reader - path "secret/data/*" { capabilities = ["read", "list"] } path "secret/metadata/*" { capabilities = ["read", "list"] } path "secret2/data/*" { capabilities = ["read", "list"] } path "secret2/metadata/*" { capabilities = ["read", "list"] } EOF - name: Create Vault policy for writer run: | cat <<EOF | vault policy write vault-sync-writer - path "secret/data/*" { capabilities = ["create", "read", "update", "delete"] } path "secret2/data/*" { capabilities = ["create", "read", "update", "delete"] } EOF - name: Create Vault AppRoles for reader and writer run: | vault write auth/approle/role/vault-sync-reader token_policies=vault-sync-reader vault write auth/approle/role/vault-sync-writer token_policies=vault-sync-writer - name: Prepare environments variables for token authentication run: | cat <<EOF > ./vault-sync-token.env export VAULT_SYNC_SRC_TOKEN=unsafe-root-token export VAULT_SYNC_DST_TOKEN=unsafe-root-token EOF - name: Prepare environments variables for approle authentication run: | cat <<EOF > ./vault-sync-app-role.env export VAULT_SYNC_SRC_ROLE_ID=$(vault read auth/approle/role/vault-sync-reader/role-id -format=json | jq -r .data.role_id) export VAULT_SYNC_SRC_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-reader/secret-id -format=json | jq -r .data.secret_id) export VAULT_SYNC_DST_ROLE_ID=$(vault read auth/approle/role/vault-sync-writer/role-id -format=json | jq -r .data.role_id) export VAULT_SYNC_DST_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-writer/secret-id -format=json | jq -r .data.secret_id) EOF - name: Create configuration file for vault-sync run: | cat <<EOF > ./vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src dst: url: http://127.0.0.1:8200/ prefix: dst EOF - name: Test sync once with token run: | vault kv put secret/src/testsecret1 foo1=bar1 source ./vault-sync-token.env cargo run -- --config vault-sync.yaml --once vault kv get secret/dst/testsecret1 - name: Test sync once with approle run: | vault kv put secret/src/testsecret2 foo2=bar2 source ./vault-sync-app-role.env cargo run -- --config vault-sync.yaml --once vault kv get secret/dst/testsecret2 - name: Create configuration file for vault-sync (custom secrets engine) run: | cat <<EOF > ./vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src backend: secret2 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret2 EOF - name: Test sync once with token run: | vault kv put -mount secret2 src/testsecret1 foo1=bar1 source ./vault-sync-token.env cargo run -- --config vault-sync.yaml --once vault kv get -mount secret2 dst/testsecret1 - name: Test sync once with approle run: | vault kv put -mount secret2 src/testsecret2 foo2=bar2 source ./vault-sync-app-role.env cargo run -- --config vault-sync.yaml --once vault kv get -mount secret dst/testsecret2 07070100000004000081A4000000000000000000000001665A0E0D000004AC000000000000000000000000000000000000002E00000000vault-sync-0.9.2/.github/workflows/docker.ymlname: Docker on: push: branches: - main tags: - '*' pull_request: branches: - main jobs: docker: name: Docker runs-on: ubuntu-latest defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set Docker Tag id: tag run: | if [[ $GITHUB_REF == refs/heads/main ]]; then DOCKER_TAG="main" else DOCKER_TAG="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml)" fi echo "tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT - name: Build Docker Image run: docker build -t pbchekin/vault-sync:${{ steps.tag.outputs.tag }} -f docker/Dockerfile . - name: Login to Docker Hub if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') run: docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password ${{ secrets.DOCKERHUB_PASSWORD }} - name: Push Docker Image if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') run: docker push pbchekin/vault-sync:${{ steps.tag.outputs.tag }} 07070100000005000081A4000000000000000000000001665A0E0D00000035000000000000000000000000000000000000001C00000000vault-sync-0.9.2/.gitignore/target /.idea vault-sync.yaml vault*.log vault*.pid 07070100000006000081A4000000000000000000000001665A0E0D00009270000000000000000000000000000000000000001C00000000vault-sync-0.9.2/Cargo.lock# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytes" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", "num-integer", "num-traits", "time 0.1.45", "wasm-bindgen", "winapi", ] [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", "bitflags", "strsim", "textwrap", "unicode-width", "vec_map", ] [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "ctrlc" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7394a21d012ce5c850497fb774b167d81b99f060025fbf06ee92b9848bd97eb2" dependencies = [ "nix", "windows-sys 0.48.0", ] [[package]] name = "encoding_rs" version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys 0.48.0", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-sink" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-io", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "h2" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap 1.9.3", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "hashicorp_vault" version = "2.1.1" dependencies = [ "base64 0.13.1", "chrono", "log", "quick-error", "reqwest", "serde", "serde_derive", "serde_json", "url", ] [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "http" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper", "native-tls", "tokio", "tokio-native-tls", ] [[package]] name = "iana-time-zone" version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "idna" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "indexmap" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown 0.14.1", ] [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.45.0", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", "cfg-if", "libc", "static_assertions", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ "hermit-abi 0.2.6", "libc", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro2" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "reqwest" version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ "base64 0.21.2", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", "http", "http-body", "hyper", "hyper-tls", "ipnet", "js-sys", "log", "mime", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "winreg", ] [[package]] name = "rustix" version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ "windows-sys 0.42.0", ] [[package]] name = "security-framework" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_repr" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ "indexmap 2.0.2", "itoa", "ryu", "serde", "unsafe-libyaml", ] [[package]] name = "simplelog" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", "termcolor", "time 0.3.21", ] [[package]] name = "slab" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "socket2" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.45.0", ] [[package]] name = "termcolor" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ "unicode-width", ] [[package]] name = "time" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "time" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "libc", "num_threads", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", "socket2", "windows-sys 0.48.0", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "unicode-bidi" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unsafe-libyaml" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] name = "url" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "vault-sync" version = "0.9.2" dependencies = [ "clap", "ctrlc", "hashicorp_vault", "log", "serde", "serde_json", "serde_repr", "serde_yaml", "simplelog", ] [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ "log", "try-lock", ] [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "web-sys" version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ "windows-targets 0.48.0", ] [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.0", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", "windows_i686_gnu 0.48.0", "windows_i686_msvc 0.48.0", "windows_x86_64_gnu 0.48.0", "windows_x86_64_gnullvm 0.48.0", "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winreg" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] 07070100000007000081A4000000000000000000000001665A0E0D0000019B000000000000000000000000000000000000001C00000000vault-sync-0.9.2/Cargo.toml[package] name = "vault-sync" version = "0.9.2" authors = ["Pavel Chekin <pbchekin@gmail.com>"] edition = "2021" [dependencies] clap = "2.34.0" ctrlc = { version = "3.2.3", features = ["termination"] } log = "0.4.17" serde = { version = "1.0.144", features = ["derive"] } serde_json = "1.0.107" serde_repr = "0.1.16" serde_yaml = "0.9.25" simplelog = "0.12.0" [dependencies.hashicorp_vault] path = "vault-rs" 07070100000008000081A4000000000000000000000001665A0E0D00002C4F000000000000000000000000000000000000001900000000vault-sync-0.9.2/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 Pavel Chekin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 07070100000009000081A4000000000000000000000001665A0E0D000017FE000000000000000000000000000000000000001B00000000vault-sync-0.9.2/README.md# vault-sync A poor man's tool to replicate secrets from one Vault instance to another. ## How it works When vault-sync starts, it does a full copy of the secrets from the source Vault instance to the destination Vault instance. Periodically, vault-sync does a full reconciliation to make sure all the destination secrets are up to date. At the same time, you can manually enable the [Socket Audit Device](https://www.vaultproject.io/docs/audit/socket) for the source Vault, so Vault will be sending audit logs to vault-sync. Using these audit logs, vault-sync keeps the secrets in the destination Vault up to date. Note that vault-sync does not create or delete the audit devices by itself. It is possible to use the same Vault instance as the source and the destination. You can use this feature to replicate a "folder" of secrets to another "folder" on the same server. You need to specify different prefixes (`src.prefix` and `dst.prefix`) in the configuration file to make sure the source and the destination do not overlap. ## Limitations * Only two Vault auth methods are supported: [Token](https://www.vaultproject.io/docs/auth/token) and [AppRole](https://www.vaultproject.io/docs/auth/approle) * Only secrets are replicated (specifically their latest versions) * Deleting secrets is not supported ## Configuration Use the [example](vault-sync.example.yaml) to create your own configuration file. Instead of specifying secrets in the configuration file, you can use environment variables: * For Token auth method: * `VAULT_SYNC_SRC_TOKEN` * `VAULT_SYNC_DST_TOKEN` * For AppRole auth method: * `VAULT_SYNC_SRC_ROLE_ID` * `VAULT_SYNC_SRC_SECRET_ID` * `VAULT_SYNC_DST_ROLE_ID` * `VAULT_SYNC_DST_SECRET_ID` ### Source Vault A token or AppRole for the source Vault should have a policy that allows listing and reading secrets: For [KV secrets engine v1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1): ```shell cat <<EOF | vault policy write vault-sync-src - path "secret/*" { capabilities = ["read", "list"] } EOF ``` For [KV secrets engine v2](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2): ```shell cat <<EOF | vault policy write vault-sync-src - path "secret/data/*" { capabilities = ["read", "list"] } path "secret/metadata/*" { capabilities = ["read", "list"] } EOF ``` If the secrets engine mounted to a custom path instead of "secret", then replace "secret" above with the custom path. To create a token for vault-sync for the source Vault: ```shell vault token create -policy=vault-sync-src ``` To enable AppRole auth method and create AppRole for vault-sync for the source Vault: ```shell # Enable approle auth method vault auth enable approle # Create a new approle and assign the policy vault write auth/approle/role/vault-sync-src token_policies=vault-sync-src # Get role id vault read auth/approle/role/vault-sync-src/role-id # Get secret id vault write -f auth/approle/role/vault-sync-src/secret-id ``` Enabling audit log: if you want to use the Vault audit device for vault-sync, then you need to create an audit device that always works. If you have only one audit device enabled, and it is not working (for example, vault-sync has terminated), then Vault will be unresponsive. Vault will not complete any requests until the audit device can write. If you have more than one audit device, then Vault will complete the request as long as one audit device persists the log. The simples way to create an audit device that always works: ```shell vault audit enable -path stdout file file_path=stdout ``` Then, when vault-sync is running, create the audit device that will be sending audit logs to vault-sync: ```shell vault audit enable -path vault-sync socket socket_type=tcp address=vault-sync:8202 ``` The device name is `vault-sync`, use the same value as specified for `id` in the configuration file. For `address`, specify the external endpoint for vault-sync. Note that vault-sync should be running and accessible via the specified address, otherwise Vault will not create the audit device. ### Destination Vault A token or AppRole for the source Vault should have a policy that allows operations on secrets: For [KV secrets engine v1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1): ```shell cat <<EOF | vault policy write vault-sync-dst - path "secret/*" { capabilities = ["create", "read", "update", "delete"] } EOF ``` For [KV secrets engine v2](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2): ```shell cat <<EOF | vault policy write vault-sync-dst - path "secret/data/*" { capabilities = ["create", "read", "update", "delete"] } EOF ``` If the secrets engine mounted to a custom path instead of "secret", then replace "secret" above with the custom path. To create a token for vault-sync for the source Vault: ```shell vault token create -policy=vault-sync-dst ``` To enable AppRole auth method and create AppRole for vault-sync for the source Vault: ```shell # Enable approle auth method vault auth enable approle # Create a new approle and assign the policy vault write auth/approle/role/vault-sync-dst token_policies=vault-sync-dst # Get role id vault read auth/approle/role/vault-sync-dst/role-id # Get secret id vault write -f auth/approle/role/vault-sync-dst/secret-id ``` ## Running ```shell vault-sync --config vault-sync.yaml ``` Command line options: * `--dry-run` vault-sync shows all the changes it is going to make to the destination Vault, but does not do any actual changes. * `--once` runs the full sync once, then exits. ## Installation ### From source code ```shell cargo build --release ``` ### Docker Assuming your configuration file `vault-sync.yaml` is in the current directory: ```shell docker run -it -v $PWD:/vault-sync pbchekin/vault-sync:0.8.0 \ vault-sync --config /vault-sync/vault-sync.yaml ``` ### Helm chart ```shell git clone https://github.com/pbchekin/vault-sync.git cd install/helm/vault-sync/ kubectl create ns vault-sync kubens vault-sync # create myvalues.yaml, using values.yaml as the example helm install vault-sync -f myvalues.yaml . ``` 0707010000000A000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001800000000vault-sync-0.9.2/docker0707010000000B000081A4000000000000000000000001665A0E0D00000137000000000000000000000000000000000000002300000000vault-sync-0.9.2/docker/DockerfileFROM rust:1.69.0 AS builder WORKDIR /usr/src/vault-sync COPY . . RUN cargo install --path . FROM debian:buster-slim RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/local/cargo/bin/vault-sync /usr/local/bin/vault-sync CMD ["vault-sync"] 0707010000000C000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001900000000vault-sync-0.9.2/install0707010000000D000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001E00000000vault-sync-0.9.2/install/helm0707010000000E000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000002900000000vault-sync-0.9.2/install/helm/vault-sync0707010000000F000081A4000000000000000000000001665A0E0D0000015D000000000000000000000000000000000000003500000000vault-sync-0.9.2/install/helm/vault-sync/.helmignore# Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ 07070100000010000081A4000000000000000000000001665A0E0D0000044C000000000000000000000000000000000000003400000000vault-sync-0.9.2/install/helm/vault-sync/Chart.yamlapiVersion: v2 name: vault-sync description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # # Application charts are a collection of templates that can be packaged into versioned archives # to be deployed. # # Library charts provide useful utilities or functions for the chart developer. They're included as # a dependency of application charts to inject those utilities and functions into the rendering # pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.1.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. appVersion: 0.9.1 07070100000011000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000003300000000vault-sync-0.9.2/install/helm/vault-sync/templates07070100000012000081A4000000000000000000000001665A0E0D00000639000000000000000000000000000000000000003D00000000vault-sync-0.9.2/install/helm/vault-sync/templates/NOTES.txt1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "vault-sync.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "vault-sync.fullname" . }}' export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "vault-sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "vault-sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 {{- end }} 07070100000013000081A4000000000000000000000001665A0E0D00000714000000000000000000000000000000000000004000000000vault-sync-0.9.2/install/helm/vault-sync/templates/_helpers.tpl{{/* Expand the name of the chart. */}} {{- define "vault-sync.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "vault-sync.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "vault-sync.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "vault-sync.labels" -}} helm.sh/chart: {{ include "vault-sync.chart" . }} {{ include "vault-sync.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "vault-sync.selectorLabels" -}} app.kubernetes.io/name: {{ include "vault-sync.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "vault-sync.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "vault-sync.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} 07070100000014000081A4000000000000000000000001665A0E0D000000E1000000000000000000000000000000000000004200000000vault-sync-0.9.2/install/helm/vault-sync/templates/configmap.yamlapiVersion: v1 kind: ConfigMap metadata: name: {{ include "vault-sync.fullname" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} data: vault-sync.yaml: | {{- toYaml .Values.vaultSync | nindent 4 }} 07070100000015000081A4000000000000000000000001665A0E0D00000932000000000000000000000000000000000000004300000000vault-sync-0.9.2/install/helm/vault-sync/templates/deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: {{ include "vault-sync.fullname" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "vault-sync.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "vault-sync.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "vault-sync.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["vault-sync", "--config", "/config/vault-sync.yaml"] volumeMounts: - name: config-volume mountPath: /config ports: - name: tcp containerPort: {{ (split ":" .Values.vaultSync.bind)._1 }} protocol: TCP resources: {{- toYaml .Values.resources | nindent 12 }} env: - name: RUST_LOG value: info {{- $secretName := include "vault-sync.fullname" . }} {{- range $key, $value := .Values.secrets }} - name: {{ $key }} valueFrom: secretKeyRef: key: {{ $key }} name: {{ $secretName }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} volumes: - name: config-volume configMap: name: {{ include "vault-sync.fullname" . }} 07070100000016000081A4000000000000000000000001665A0E0D00000395000000000000000000000000000000000000003C00000000vault-sync-0.9.2/install/helm/vault-sync/templates/hpa.yaml{{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: {{ include "vault-sync.fullname" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "vault-sync.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: name: cpu targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - type: Resource resource: name: memory targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} 07070100000017000081A4000000000000000000000001665A0E0D00000422000000000000000000000000000000000000004000000000vault-sync-0.9.2/install/helm/vault-sync/templates/ingress.yaml{{- if .Values.ingress.enabled -}} {{- $fullName := include "vault-sync.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "vault-sync.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ . }} backend: serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} 07070100000018000081A4000000000000000000000001665A0E0D000000D2000000000000000000000000000000000000003F00000000vault-sync-0.9.2/install/helm/vault-sync/templates/secret.yamlapiVersion: v1 kind: Secret metadata: name: {{ include "vault-sync.fullname" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} type: Opaque data: {{- toYaml .Values.secrets | nindent 2 }} 07070100000019000081A4000000000000000000000001665A0E0D00000172000000000000000000000000000000000000004000000000vault-sync-0.9.2/install/helm/vault-sync/templates/service.yamlapiVersion: v1 kind: Service metadata: name: {{ include "vault-sync.fullname" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "vault-sync.selectorLabels" . | nindent 4 }} 0707010000001A000081A4000000000000000000000001665A0E0D00000146000000000000000000000000000000000000004700000000vault-sync-0.9.2/install/helm/vault-sync/templates/serviceaccount.yaml{{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "vault-sync.serviceAccountName" . }} labels: {{- include "vault-sync.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} 0707010000001B000081A4000000000000000000000001665A0E0D00000949000000000000000000000000000000000000003500000000vault-sync-0.9.2/install/helm/vault-sync/values.yaml# Default values for vault-sync. # This is a YAML-formatted file. # Declare variables to be passed into your templates. vaultSync: id: vault-sync full_sync_interval: 3600 # bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ prefix: "" backend: secret # token_ttl: 86400 # token_max_ttl: 2764800 dst: url: http://127.0.0.1:8200/ prefix: "" backend: secret # token_ttl: 86400 # token_max_ttl: 2764800 # Secrets must be base64 encoded secrets: VAULT_SYNC_SRC_TOKEN: xxx # VAULT_SYNC_SRC_ROLE_ID: xxx # VAULT_SYNC_SRC_SECRET_ID: xxx VAULT_SYNC_DST_TOKEN: xxx # VAULT_SYNC_DST_ROLE_ID: xxx # VAULT_SYNC_DST_SECRET_ID: xxx replicaCount: 1 image: repository: pbchekin/vault-sync pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "0.9.2" imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 service: type: ClusterIP port: 8202 ingress: enabled: false annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: [] tls: [] # - secretName: chart-example-tls # hosts: # - chart-example.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 nodeSelector: {} tolerations: [] affinity: {} 0707010000001C000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001900000000vault-sync-0.9.2/scripts0707010000001D000081ED000000000000000000000001665A0E0D000024F8000000000000000000000000000000000000002100000000vault-sync-0.9.2/scripts/test.sh#!/bin/bash # Local test. # Requires installed vault. # TODO: use vault container instead of locally installed vault. set -e -o pipefail vault server -dev -dev-root-token-id=unsafe-root-token &> vault.log & echo $! > vault.pid function cleanup() {( set -e if [[ -f vault.pid ]]; then kill $(<vault.pid) || true rm -f vault.pid fi if [[ -f vault-sync.pid ]]; then kill $(<vault-sync.pid) || true rm -f vault-sync.pid fi )} trap cleanup EXIT export VAULT_ADDR='http://127.0.0.1:8200' # Make sure Vault is running while ! vault token lookup; do sleep 1; done # Enable AppRole auth method vault auth enable approle # Enable secrets engine v1 at the path "secret1" vault secrets enable -version=1 -path=secret1 kv # Enable secrets engine v2 at the path "secret2" vault secrets enable -version=2 -path=secret2 kv vault secrets list # Check all secret engines are enabled vault secrets list | grep -qE '^secret/\s+kv' vault secrets list | grep -qE '^secret1/\s+kv' vault secrets list | grep -qE '^secret2/\s+kv' # Create reader policy cat <<EOF | vault policy write vault-sync-reader - # Default secret backend "secret" (kv version 2) path "secret/data/*" { capabilities = ["read", "list"] } path "secret/metadata/*" { capabilities = ["read", "list"] } # Custom secret backend "secret1" (kv version 1) path "secret1/*" { capabilities = ["read", "list"] } # Custom secret backend "secret2" (kv version 2) path "secret2/data/*" { capabilities = ["read", "list"] } path "secret2/metadata/*" { capabilities = ["read", "list"] } EOF # Create writer policy cat <<EOF | vault policy write vault-sync-writer - # Default secret backend "secret" (kv version 2) path "secret/data/*" { capabilities = ["create", "read", "update", "delete"] } # Custom secret backend "secret1" (kv version 1) path "secret1/*" { capabilities = ["create", "read", "update", "delete"] } # Custom secret backend "secret2" (kv version 2) path "secret2/data/*" { capabilities = ["create", "read", "update", "delete"] } EOF # Create new AppRoles vault write auth/approle/role/vault-sync-reader token_policies=vault-sync-reader vault write auth/approle/role/vault-sync-writer token_policies=vault-sync-writer cat <<EOF > /tmp/vault-sync-token.env export VAULT_SYNC_SRC_TOKEN=unsafe-root-token export VAULT_SYNC_DST_TOKEN=unsafe-root-token EOF cat <<EOF > /tmp/vault-sync-app-role.env export VAULT_SYNC_SRC_ROLE_ID=$(vault read auth/approle/role/vault-sync-reader/role-id -format=json | jq -r .data.role_id) export VAULT_SYNC_SRC_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-reader/secret-id -format=json | jq -r .data.secret_id) export VAULT_SYNC_DST_ROLE_ID=$(vault read auth/approle/role/vault-sync-writer/role-id -format=json | jq -r .data.role_id) export VAULT_SYNC_DST_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-writer/secret-id -format=json | jq -r .data.secret_id) EOF function test_token() {( local src_backend=$1 local dst_backend=${2:-$src_backend} local secret_name=test-$RANDOM vault kv put -mount $src_backend ${src_prefix}${secret_name} foo=bar source /tmp/vault-sync-token.env cargo run -- --config /tmp/vault-sync.yaml --once vault kv get -mount $dst_backend ${dst_prefix}${secret_name} vault kv get -mount $dst_backend ${dst_prefix}${secret_name} | grep -qE '^foo\s+bar$' )} function test_app_role() {( local src_backend=$1 local dst_backend=${2:-$src_backend} local secret_name=test-$RANDOM vault kv put -mount $src_backend ${src_prefix}${secret_name} foo=bar source /tmp/vault-sync-app-role.env cargo run -- --config /tmp/vault-sync.yaml --once vault kv get -mount $dst_backend ${dst_prefix}${secret_name} vault kv get -mount $dst_backend ${dst_prefix}${secret_name} | grep -qE '^foo\s+bar$' )} function test_token_with_audit_device() { local src_backend=$1 local dst_backend=${2:-$src_backend} local secret_name=test-$RANDOM source /tmp/vault-sync-token.env vault kv put -mount $src_backend ${src_prefix}${secret_name}-1 foo=bar cargo run -- --config /tmp/vault-sync.yaml & echo $! > vault-sync.pid echo Wating for vault-sync to start and make the initial sync ... VAULT_SYNC_READY="" for i in 1 2 3 4 5; do if vault kv get -mount $dst_backend ${dst_prefix}${secret_name}-1 2> /dev/null; then vault kv get -mount $dst_backend ${dst_prefix}${secret_name}-1 | grep -qE '^foo\s+bar$' VAULT_SYNC_READY="true" break fi sleep 1 done if [[ ! $VAULT_SYNC_READY ]]; then echo "vault-sync failed to start with audit device" exit 1 fi # Enable audit device that sends log to vault-sync vault audit enable -path vault-sync-$src_backend socket socket_type=tcp address=127.0.0.1:8202 vault kv put -mount $src_backend ${dst_prefix}${secret_name}-2 foo=bar echo Wating for vault-sync to sync on event ... VAULT_SYNC_READY="" for i in 1 2 3 4 5; do if vault kv get -mount $dst_backend ${dst_prefix}/${secret_name}-2 2> /dev/null; then vault kv get -mount $dst_backend ${dst_prefix}/${secret_name}-2 | grep -qE '^foo\s+bar$' VAULT_SYNC_READY="true" break fi sleep 1 done if [[ ! $VAULT_SYNC_READY ]]; then echo "vault-sync failed to sync on the event from the audit device" exit 1 fi vault audit disable vault-sync-$src_backend kill $(<vault-sync.pid) rm vault-sync.pid } # secret/src -> secret/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src dst: url: http://127.0.0.1:8200/ prefix: dst EOF src_prefix="src/" dst_prefix="dst/" test_token secret test_app_role secret # secret1/src -> secret1/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src backend: secret1 version: 1 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret1 version: 1 EOF src_prefix="src/" dst_prefix="dst/" test_token secret1 test_app_role secret1 # secret2/src -> secret2/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src backend: secret2 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret2 EOF src_prefix="src/" dst_prefix="dst/" test_token secret2 test_app_role secret2 # secret1/src -> secret2/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src backend: secret1 version: 1 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret2 EOF src_prefix="src/" dst_prefix="dst/" test_token secret1 secret2 test_app_role secret1 secret2 # secret2/src -> secret1/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ prefix: src backend: secret2 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret1 version: 1 EOF src_prefix="src/" dst_prefix="dst/" test_token secret2 secret1 test_app_role secret2 secret1 # secret1 -> secret2 cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ backend: secret1 version: 1 dst: url: http://127.0.0.1:8200/ backend: secret2 EOF src_prefix="" dst_prefix="" test_token secret1 secret2 test_app_role secret1 secret2 # secret2 -> secret1 cat <<EOF > /tmp/vault-sync.yaml id: vault-sync full_sync_interval: 10 src: url: http://127.0.0.1:8200/ backend: secret2 dst: url: http://127.0.0.1:8200/ backend: secret1 version: 1 EOF src_prefix="" dst_prefix="" test_token secret2 secret1 test_app_role secret2 secret1 # Enable audit device that always works vault audit enable -path vault-audit file file_path=vault-audit.log # secret/src -> secret/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync-secret bind: 0.0.0.0:8202 full_sync_interval: 60 src: url: http://127.0.0.1:8200/ prefix: src dst: url: http://127.0.0.1:8200/ prefix: dst EOF src_prefix="src/" dst_prefix="dst/" test_token_with_audit_device secret # secret1/src -> secret1/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync-secret2 full_sync_interval: 60 bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ prefix: src backend: secret1 version: 1 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret1 version: 1 EOF src_prefix="src/" dst_prefix="dst/" test_token_with_audit_device secret1 # secret2/src -> secret2/dst cat <<EOF > /tmp/vault-sync.yaml id: vault-sync-secret2 full_sync_interval: 60 bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ prefix: src backend: secret2 dst: url: http://127.0.0.1:8200/ prefix: dst backend: secret2 EOF src_prefix="src/" dst_prefix="dst/" test_token_with_audit_device secret2 # secret1 -> secret2 cat <<EOF > /tmp/vault-sync.yaml id: vault-sync-secret1 full_sync_interval: 60 bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ backend: secret1 version: 1 dst: url: http://127.0.0.1:8200/ backend: secret2 EOF src_prefix="" dst_prefix="" test_token_with_audit_device secret1 secret2 # secret2 -> secret1 cat <<EOF > /tmp/vault-sync.yaml id: vault-sync-secret2 full_sync_interval: 60 bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ backend: secret2 dst: url: http://127.0.0.1:8200/ backend: secret1 version: 1 EOF src_prefix="" dst_prefix="" test_token_with_audit_device secret2 secret1 0707010000001E000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001500000000vault-sync-0.9.2/src0707010000001F000081A4000000000000000000000001665A0E0D0000026F000000000000000000000000000000000000001E00000000vault-sync-0.9.2/src/audit.rsuse serde::{Deserialize, Serialize}; #[derive(Deserialize, Debug)] pub struct AuditLog { pub time: String, #[serde(rename = "type")] pub log_type: String, pub request: Request, } #[derive(Deserialize, Debug)] pub struct Request { pub operation: String, pub mount_type: Option<String>, pub path: String, } #[derive(Serialize, Debug)] pub struct CreateAuditDeviceRequest { #[serde(rename = "type")] pub audit_device_type: String, pub options: AuditDeviceOptions, } #[derive(Serialize, Debug)] pub struct AuditDeviceOptions { pub address: String, pub socket_type: String, } 07070100000020000081A4000000000000000000000001665A0E0D00001151000000000000000000000000000000000000001F00000000vault-sync-0.9.2/src/config.rsuse std::env; use std::error::Error; use std::fmt; use std::fmt::Formatter; use std::fs::File; use serde::{Deserialize, Serialize, Serializer}; use serde_repr::*; #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(untagged)] pub enum VaultAuthMethod { TokenAuth { #[serde(serialize_with = "sanitize")] token: String, }, AppRoleAuth { #[serde(serialize_with = "sanitize")] role_id: String, #[serde(serialize_with = "sanitize")] secret_id: String, } } #[derive(Serialize_repr, Deserialize_repr, PartialEq, Clone, Debug)] #[repr(u8)] pub enum EngineVersion { V1 = 1, V2 = 2, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct VaultHost { pub url: String, #[serde(flatten)] pub auth: Option<VaultAuthMethod>, pub token_ttl: Option<u64>, pub token_max_ttl: Option<u64>, } #[derive(Serialize, Deserialize, Debug)] pub struct VaultSource { #[serde(flatten)] pub host: VaultHost, #[serde(default)] pub prefix: String, #[serde(default = "default_backend")] pub backend: String, #[serde(default)] pub version: EngineVersion, } #[derive(Serialize, Deserialize, Debug)] pub struct VaultDestination { #[serde(flatten)] pub host: VaultHost, #[serde(default)] pub prefix: String, #[serde(default = "default_backend")] pub backend: String, #[serde(default)] pub version: EngineVersion, } #[derive(Serialize, Deserialize, Debug)] pub struct VaultSyncConfig { pub id: String, pub full_sync_interval: u64, pub bind: Option<String>, pub src: VaultSource, pub dst: VaultDestination, } #[derive(Debug, Clone)] pub enum ConfigError { AuthRequired, } impl Default for EngineVersion { fn default() -> Self { EngineVersion::V2 } } impl VaultSyncConfig { pub fn from_file(file_name: &str) -> Result<VaultSyncConfig, Box<dyn Error>> { let file = File::open(file_name)?; let mut config: VaultSyncConfig = serde_yaml::from_reader(file)?; config.auth_from_env()?; Ok(config) } fn auth_from_env(&mut self) -> Result<(), Box<dyn Error>>{ if self.src.host.auth.is_none() { self.src.host.auth = Some(VaultAuthMethod::from_env("VAULT_SYNC_SRC")?); } if self.dst.host.auth.is_none() { self.dst.host.auth = Some(VaultAuthMethod::from_env("VAULT_SYNC_DST")?); } Ok(()) } } impl VaultAuthMethod { fn from_env(prefix: &str) -> Result<VaultAuthMethod, Box<dyn Error>> { let token = env::var(format!("{}_TOKEN", prefix)); let role_id = env::var(format!("{}_ROLE_ID", prefix)); let secret_id = env::var(format!("{}_SECRET_ID", prefix)); if let Ok(token) = token { return Ok(VaultAuthMethod::TokenAuth { token }) } if let (Ok(role_id), Ok(secret_id)) = (role_id, secret_id) { return Ok(VaultAuthMethod::AppRoleAuth { role_id, secret_id }) } Err(ConfigError::AuthRequired.into()) } } impl fmt::Display for ConfigError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match *self { ConfigError::AuthRequired => write!(f, "Vault token or both app role id and secret id are required"), } } } impl Error for ConfigError { } fn sanitize<S>(_: &str, s: S) -> Result<S::Ok, S::Error> where S: Serializer, { s.serialize_str("***") } fn default_backend() -> String { "secret".to_string() } #[cfg(test)] mod tests { use crate::config::{EngineVersion, VaultSyncConfig}; #[test] fn test_load() { let yaml = r#" id: vault-sync-id full_sync_interval: 60 bind: 0.0.0.0:8202 src: url: http://127.0.0.1:8200/ prefix: src dst: url: http://127.0.0.1:8200/ prefix: dst backend: custom version: 1 "#; let config: VaultSyncConfig = serde_yaml::from_str(yaml).unwrap(); assert_eq!(config.id, "vault-sync-id"); assert_eq!(config.bind, Some("0.0.0.0:8202".to_string())); assert_eq!(config.src.backend, "secret"); assert_eq!(config.dst.backend, "custom"); assert_eq!(config.src.version, EngineVersion::V2); assert_eq!(config.dst.version, EngineVersion::V1); } }07070100000021000081A4000000000000000000000001665A0E0D0000183B000000000000000000000000000000000000001D00000000vault-sync-0.9.2/src/main.rsuse std::{thread, time}; use std::error::Error; use std::net::TcpListener; use std::sync::{Arc, Mutex}; use std::sync::mpsc; use clap::{crate_authors, crate_version, Arg, App}; use hashicorp_vault::client::SecretsEngine; use log::{error, info}; use simplelog::*; use config::{VaultHost, VaultSyncConfig}; use vault::VaultClient; use crate::config::EngineVersion; mod audit; mod config; mod sync; mod vault; fn main() -> Result<(), Box<dyn Error>> { TermLogger::init(LevelFilter::Info, Config::default(), TerminalMode::Mixed, ColorChoice::Auto)?; let matches = App::new("vault-sync") .author(crate_authors!()) .version(crate_version!()) .arg(Arg::with_name("config") .long("config") .value_name("FILE") .help("Configuration file") .default_value("./vault-sync.yaml") .takes_value(true)) .arg(Arg::with_name("dry-run") .long("dry-run") .help("Do not do any changes with the destination Vault")) .arg(Arg::with_name("once") .long("once") .help("Run the full sync once, then exit")) .get_matches(); let config = load_config(matches.value_of("config").unwrap())?; let (tx, rx): (mpsc::Sender<sync::SecretOp>, mpsc::Receiver<sync::SecretOp>) = mpsc::channel(); let log_sync = if let Some(bind) = &config.bind { Some(log_sync_worker(bind, &config.src.prefix, &config.src.backend, &config.src.version, tx.clone())?) } else { None }; info!("Connecting to {}", &config.src.host.url); let mut src_client = vault_client(&config.src.host)?; src_client.secret_backend(&config.src.backend); src_client.secrets_engine( match config.src.version { EngineVersion::V1 => SecretsEngine::KVV1, EngineVersion::V2 => SecretsEngine::KVV2, } ); info!( "Audit device {} exists: {}", &config.id, sync::audit_device_exists(&config.id, &src_client), ); let shared_src_client = Arc::new(Mutex::new(src_client)); let src_token = token_worker(&config.src.host, shared_src_client.clone()); info!("Connecting to {}", &config.dst.host.url); let mut dst_client = vault_client(&config.dst.host)?; dst_client.secret_backend(&config.dst.backend); dst_client.secrets_engine( match config.dst.version { EngineVersion::V1 => SecretsEngine::KVV1, EngineVersion::V2 => SecretsEngine::KVV2, } ); let shared_dst_client = Arc::new(Mutex::new(dst_client)); let dst_token = token_worker(&config.dst.host, shared_dst_client.clone()); let sync = sync_worker( rx, &config.src.prefix, &config.dst.prefix, shared_src_client.clone(), shared_dst_client.clone(), matches.is_present("dry-run"), matches.is_present("once"), ); let mut join_handlers = vec![sync]; if !matches.is_present("once") { let full_sync = full_sync_worker(&config, shared_src_client.clone(), tx.clone()); join_handlers.push(full_sync); join_handlers.push(src_token); join_handlers.push(dst_token); if log_sync.is_some() { join_handlers.push(log_sync.unwrap()); } } else { sync::full_sync(&config.src.prefix, shared_src_client.clone(), tx.clone()); }; // Join all threads for handler in join_handlers { let _ = handler.join(); } Ok(()) } fn load_config(file_name: &str) -> Result<VaultSyncConfig, Box<dyn Error>> { match VaultSyncConfig::from_file(file_name) { Ok(config) => { info!("Configuration from {}:\n{}", file_name, serde_json::to_string_pretty(&config).unwrap()); Ok(config) }, Err(error) => { error!("Failed to load configuration file {}: {}", file_name, error); Err(error) } } } fn vault_client(host: &VaultHost) -> Result<VaultClient, Box<dyn Error>> { match vault::vault_client(host) { Ok(client) => { Ok(client) }, Err(error) => { error!("Failed to connect to {}: {}", &host.url, error); Err(error.into()) } } } fn token_worker(host: &VaultHost, client: Arc<Mutex<VaultClient>>) -> thread::JoinHandle<()> { let host = host.clone(); thread::spawn(move || { vault::token_worker(&host, client); }) } fn sync_worker( rx: mpsc::Receiver<sync::SecretOp>, src_prefix: &str, dst_prefix: &str, src_client: Arc<Mutex<VaultClient>>, dst_client: Arc<Mutex<VaultClient>>, dry_run: bool, run_once: bool, ) -> thread::JoinHandle<()> { info!("Dry run: {}", dry_run); let src_prefix = src_prefix.to_string(); let dst_prefix = dst_prefix.to_string(); thread::spawn(move || { sync::sync_worker(rx, &src_prefix, &dst_prefix, src_client, dst_client, dry_run, run_once); }) } fn log_sync_worker(addr: &str, prefix: &str, backend: &str, version: &EngineVersion, tx: mpsc::Sender<sync::SecretOp>) -> Result<thread::JoinHandle<()>, std::io::Error> { let prefix = prefix.to_string(); let backend = backend.to_string(); let version = version.clone(); info!("Listening on {}", addr); let listener = TcpListener::bind(addr)?; let handle = thread::spawn(move || { for stream in listener.incoming() { if let Ok(stream) = stream { let tx = tx.clone(); let prefix = prefix.clone(); let backend = backend.clone(); let version = version.clone(); thread::spawn(move || { sync::log_sync(&prefix, &backend, &version, stream, tx); }); } } }); Ok(handle) } fn full_sync_worker( config: &VaultSyncConfig, client: Arc<Mutex<VaultClient>>, tx: mpsc::Sender<sync::SecretOp> ) -> thread::JoinHandle<()>{ let interval = time::Duration::from_secs(config.full_sync_interval); let prefix = config.src.prefix.clone(); thread::spawn(move || { sync::full_sync_worker(&prefix, interval, client, tx); }) } 07070100000022000081A4000000000000000000000001665A0E0D0000327C000000000000000000000000000000000000001D00000000vault-sync-0.9.2/src/sync.rsuse std::{thread, time}; use std::io::{BufRead, BufReader}; use std::net::TcpStream; use std::sync::{Arc, Mutex}; use std::sync::mpsc; use hashicorp_vault::client::{EndpointResponse, HttpVerb}; use log::{debug, info, warn}; use serde_json::Value; use crate::audit; use crate::config::EngineVersion; use crate::vault::VaultClient; pub fn audit_device_exists(name: &str, client: &VaultClient) -> bool { let name = format!("{}/", name); match client.call_endpoint::<Value>(HttpVerb::GET, "sys/audit", None, None) { Ok(response) => { debug!("GET sys/audit: {:?}", response); if let EndpointResponse::VaultResponse(response) = response { if let Some(Value::Object(map)) = response.data { for (key, _) in &map { if key == &name { return true; } } } } }, Err(error) => { warn!("GET sys/audit: {}", error); } } false } pub fn full_sync_worker( prefix: &str, interval: time::Duration, client: Arc<Mutex<VaultClient>>, tx: mpsc::Sender<SecretOp> ) { info!("FullSync worker started"); loop { full_sync(prefix, client.clone(), tx.clone()); thread::sleep(interval); } } struct Item { parent: String, secrets: Option<Vec<String>>, index: usize, } pub fn full_sync(prefix: &str, client: Arc<Mutex<VaultClient>>, tx: mpsc::Sender<SecretOp>) { let prefix= normalize_prefix(prefix); info!("FullSync started"); let now = time::Instant::now(); full_sync_internal(&prefix, client, tx); info!("FullSync finished in {}ms", now.elapsed().as_millis()); } fn full_sync_internal(prefix: &str, client: Arc<Mutex<VaultClient>>, tx: mpsc::Sender<SecretOp>) { let mut stack: Vec<Item> = Vec::new(); let item = Item { parent: prefix.to_string(), secrets: None, index: 0, }; stack.push(item); 'outer: while stack.len() > 0 { let len = stack.len(); let item = stack.get_mut(len - 1).unwrap(); if item.secrets.is_none() { let secrets = { let client = client.lock().unwrap(); client.list_secrets(&item.parent) }; match secrets { Ok(secrets) => { item.secrets = Some(secrets); }, Err(error) => { warn!("Failed to list secrets in {}: {}", &item.parent, error); } } } if let Some(secrets) = &item.secrets { while item.index < secrets.len() { let secret = &secrets[item.index]; item.index += 1; if secret.ends_with("/") { let item = Item { // item.parent ends with '/' parent: format!("{}{}", &item.parent, secret), secrets: None, index: 0, }; stack.push(item); continue 'outer; } else { let full_name = format!("{}{}", &item.parent, &secret); if let Err(error) = tx.send(SecretOp::Create(full_name)) { warn!("Failed to send a secret to a sync thread: {}", error); } } } } stack.pop(); } let _ = tx.send(SecretOp::FullSyncFinished); } pub fn log_sync(prefix: &str, backend: &str, version: &EngineVersion, stream: TcpStream, tx: mpsc::Sender<SecretOp>) { match stream.peer_addr() { Ok(peer_addr) => { info!("New connection from {}", peer_addr); }, Err(_) => { info!("New connection"); } } let mut reader = BufReader::new(stream); loop { let mut line = String::new(); match reader.read_line(&mut line) { Ok(0) => { // EOF break; }, Ok(_) => { debug!("Log: '{}'", line.trim()); let audit_log: Result<audit::AuditLog, _> = serde_json::from_str(&line); match audit_log { Ok(audit_log) => { if let Some(op) = audit_log_op(backend, prefix, version, &audit_log) { if let Err(error) = tx.send(op) { warn!("Failed to send a secret to a sync thread: {}", error); } } }, Err(error) => { warn!("Failed to deserialize: {}, response: {}", error, &line); } } }, Err(error) => { warn!("Error: {}", error); break; } } } debug!("Closed connection"); } #[derive(Debug)] pub enum SecretOp { Create(String), Update(String), Delete(String), FullSyncFinished, } struct SyncStats { updated: u64, deleted: u64, } impl SyncStats { fn new() -> SyncStats { SyncStats { updated: 0, deleted: 0 } } fn reset(&mut self) { self.updated = 0; self.deleted = 0; } } pub fn sync_worker( rx: mpsc::Receiver<SecretOp>, src_prefix: &str, dst_prefix: &str, src_client: Arc<Mutex<VaultClient>>, dst_client: Arc<Mutex<VaultClient>>, dry_run: bool, run_once: bool, ) { let src_prefix = normalize_prefix(src_prefix); let dst_prefix = normalize_prefix(dst_prefix); info!("Sync worker started"); let mut stats = SyncStats::new(); loop { let op = rx.recv(); if let Ok(op) = op { match op { SecretOp::Update(src_path) | SecretOp::Create(src_path) => { let dst_path = secret_src_to_dst_path(&src_prefix, &dst_prefix, &src_path); let src_secret: Result<Value, _> = { let client = src_client.lock().unwrap(); client.get_custom_secret(&src_path) }; let dst_secret: Result<Value, _> = { let client = dst_client.lock().unwrap(); client.get_custom_secret(&dst_path) }; if let Err(error) = src_secret { warn!("Failed to get secret {}: {}", &src_path, error); continue; } let src_secret = src_secret.unwrap(); if let Ok(dst_secret) = dst_secret { if dst_secret == src_secret { continue; } } info!("Creating/updating secret {}", &dst_path); if !dry_run { let result = { let client = dst_client.lock().unwrap(); client.set_custom_secret(&dst_path, &src_secret) }; if let Err(error) = result { warn!("Failed to set secret {}: {}", &dst_path, error); } else { stats.updated += 1; } } }, SecretOp::Delete(secret) => { let secret = secret_src_to_dst_path(&src_prefix, &dst_prefix, &secret); info!("Deleting secret {}", &secret); if !dry_run { let client = dst_client.lock().unwrap(); let _ = client.delete_secret(&secret); } else { stats.deleted += 1; } }, SecretOp::FullSyncFinished => { info!("Secrets created/updated: {}, deleted: {}", &stats.updated, &stats.deleted); stats.reset(); if run_once { break; } }, } } } } // Convert AuditLog to SecretOp fn audit_log_op(mount: &str, prefix: &str, version: &EngineVersion, log: &audit::AuditLog) -> Option<SecretOp> { if log.log_type != "response" { return None; } if log.request.mount_type.is_none() { return None; } if log.request.mount_type != Some("kv".to_string()) { return None; } let operation = log.request.operation.clone(); if operation != "create" && operation != "update" && operation != "delete" { return None; } let path = match version { EngineVersion::V1 => secret_path_v1(&log.request.path), EngineVersion::V2 => secret_path_v2(&log.request.path), }; if let Some(path) = path { if path.0 != mount { return None; } if !path.1.starts_with(prefix) { return None; } if operation == "create" { return Some(SecretOp::Create(path.1)); } else if operation == "update" { return Some(SecretOp::Update(path.1)); } else if operation == "delete" { return Some(SecretOp::Delete(path.1)); } } None } // Convert Vault path to a secret path for KV v1 // Example: "secret/path/to/secret" -> "secret", "path/to/secret" fn secret_path_v1(path: &str) -> Option<(String, String)> { let parts: Vec<&str> = path.split("/").collect(); if parts.len() < 2 { return None } Some((parts[0].to_string(), parts[1..].join("/"))) } // Convert Vault path to a secret path for KV v2 // Example: "secret/data/path/to/secret" -> "secret", "path/to/secret" fn secret_path_v2(path: &str) -> Option<(String, String)> { let parts: Vec<&str> = path.split("/").collect(); if parts.len() < 3 { return None } // `vault kv metadata delete secret/path` has `metadata` instead of `data`, // we do not support this yet if parts[1] == "data" { Some((parts[0].to_string(), parts[2..].join("/"))) } else { None } } fn normalize_prefix(prefix: &str) -> String { if prefix.len() == 0 { return "".to_string(); } if prefix.ends_with("/") { prefix.to_string() } else { format!("{}/", prefix) } } // Convert source secret path to destination secret path. Prefixes must be normalized! // Example: "src/secret1" -> "dst/secret2" fn secret_src_to_dst_path(src_prefix: &str, dst_prefix: &str, path: &str) -> String { let mut path = path.to_string(); if src_prefix.len() > 0 { path = path.trim_start_matches(src_prefix).to_string(); } format!("{}{}", dst_prefix, &path) } #[cfg(test)] mod tests { use crate::sync::{normalize_prefix, secret_path_v1, secret_path_v2, secret_src_to_dst_path}; #[test] fn test_secret_path_v1_matches() { let path = "secret/path/to/secret"; let path = secret_path_v1(&path).unwrap(); assert_eq!(path.0, "secret"); assert_eq!(path.1, "path/to/secret"); } #[test] fn test_custom_secret_path_v1_matches() { let path = "custom/path/to/secret"; let path = secret_path_v1(&path).unwrap(); assert_eq!(path.0, "custom"); assert_eq!(path.1, "path/to/secret"); } #[test] fn test_secret_path_v1_not_matches() { let path = "secret"; let path = secret_path_v1(&path); assert_eq!(path.is_none(), true); } #[test] fn test_secret_path_v2_matches() { let path = "secret/data/path/to/secret"; let path = secret_path_v2(&path).unwrap(); assert_eq!(path.0, "secret"); assert_eq!(path.1, "path/to/secret"); } #[test] fn test_custom_secret_path_v2_matches() { let path = "custom/data/path/to/secret"; let path = secret_path_v2(&path).unwrap(); assert_eq!(path.0, "custom"); assert_eq!(path.1, "path/to/secret"); } #[test] fn test_secret_path_v2_not_matches() { let path = "secret/metadata/path/to/secret"; let path = secret_path_v2(&path); assert_eq!(path.is_none(), true); } #[test] fn test_normalize_prefix() { assert_eq!(normalize_prefix(""), ""); assert_eq!(normalize_prefix("src"), "src/"); assert_eq!(normalize_prefix("src/"), "src/"); } #[test] fn test_secret_src_to_dst_path() { assert_eq!(secret_src_to_dst_path("src/", "dst/", "src/secret"), "dst/secret"); assert_eq!(secret_src_to_dst_path("", "dst/", "src/secret"), "dst/src/secret"); assert_eq!(secret_src_to_dst_path("", "", "src/secret"), "src/secret"); } }07070100000023000081A4000000000000000000000001665A0E0D000016E8000000000000000000000000000000000000001E00000000vault-sync-0.9.2/src/vault.rsuse std::{thread, time}; use std::sync::{Arc, Mutex}; use std::time::Duration; use hashicorp_vault::client as vault; use hashicorp_vault::client::{TokenData, VaultDuration}; use hashicorp_vault::client::error::Result as VaultResult; use log::{info, warn}; use crate::config::{VaultAuthMethod, VaultHost}; pub type VaultClient = hashicorp_vault::client::VaultClient<TokenData>; pub fn vault_client(host: &VaultHost) -> VaultResult<vault::VaultClient<TokenData>> { match host.auth.as_ref().unwrap() { VaultAuthMethod::TokenAuth { token } => { vault::VaultClient::new(&host.url, token) }, VaultAuthMethod::AppRoleAuth { role_id, secret_id} => { let client = vault::VaultClient::new_app_role( &host.url, role_id, Some(secret_id))?; vault::VaultClient::new(&host.url, client.token) } } } // Worker to renew a Vault token lease, or to request a new token (for Vault AppRole auth method) pub fn token_worker(host: &VaultHost, client: Arc<Mutex<VaultClient>>) { let mut token_age = time::Instant::now(); loop { let info = { let client = client.lock().unwrap(); TokenInfo::from_client(&client) }; info!("Token: {:?}", &info); // Override token TTL and max TTL with optional values from config let mut plan = info.clone(); if let Some(token_ttl) = &host.token_ttl { match &info.ttl { Some(ttl) => { if *token_ttl > 0 && *token_ttl < ttl.as_secs() { plan.ttl = Some(Duration::from_secs(*token_ttl)); } }, None => { plan.ttl = Some(Duration::from_secs(*token_ttl)) } } } if let Some(token_max_ttl) = &host.token_max_ttl { match &info.max_ttl { Some(max_ttl) => { if *token_max_ttl > 0 && *token_max_ttl < max_ttl.as_secs() { plan.max_ttl = Some(Duration::from_secs(*token_max_ttl)); } }, None => { plan.max_ttl = Some(Duration::from_secs(*token_max_ttl)) } } } info!("Plan: {:?}", &plan); if !plan.renewable { return; } else { if let Some(VaultAuthMethod::AppRoleAuth { role_id: _, secret_id: _ }) = &host.auth { if plan.max_ttl.is_none() { warn!("Auth method is AppRole, but max_ttl is not set, using 32 days instead"); plan.max_ttl = Some(time::Duration::from_secs(32 * 24 * 60 * 60)); } } if let Some(VaultAuthMethod::TokenAuth { token: _ }) = &host.auth { if plan.max_ttl.is_some() { info!("Auth method is Token, but max_ttl is set, ignoring"); plan.max_ttl = None; } } } let duration = { if plan.ttl.is_none() { plan.max_ttl.unwrap() } else if plan.max_ttl.is_none() { plan.ttl.unwrap() } else { plan.ttl.unwrap().min(plan.max_ttl.unwrap()) } }; let duration = time::Duration::from_secs(duration.as_secs() / 2); thread::sleep(duration); if let Some(max_ttl) = plan.max_ttl { let age = token_age.elapsed().as_secs(); let max_ttl = max_ttl.as_secs(); if age > max_ttl / 2 { if let Some(VaultAuthMethod::AppRoleAuth { role_id: _, secret_id: _ }) = &host.auth { info!("Requesting a new token"); match vault_client(&host) { Ok(new_client) => { let mut client = client.lock().unwrap(); client.token = new_client.token; client.data = new_client.data; token_age = time::Instant::now(); continue; }, Err(error) => { warn!("Failed to request a new token: {}", error); } } } } } if let Some(_) = plan.ttl { info!("Renewing token"); let result = { let mut client = client.lock().unwrap(); client.renew() }; if let Err(error) = result { warn!("Failed to renew token: {}", error); } } } } #[derive(Debug, Clone)] struct TokenInfo { renewable: bool, ttl: Option<Duration>, max_ttl: Option<Duration>, } impl TokenInfo { fn new() -> TokenInfo { // Defaults are for the root token, which is not renewable and has no TTL and max TTL TokenInfo { renewable: false, ttl: None, max_ttl: None, } } fn from_client(client: &VaultClient) -> TokenInfo { let mut info = Self::new(); if let Some(data) = &client.data { if let Some(data) = &data.data { let zero_duration = VaultDuration::seconds(0); info.renewable = data.renewable.unwrap_or(false); let ttl_duration= &data.ttl; if ttl_duration.0.as_secs() > 0 { info.ttl = Some(ttl_duration.0); } let max_ttl_duration = data.explicit_max_ttl.as_ref().unwrap_or(&zero_duration); if max_ttl_duration.0.as_secs() > 0 { info.max_ttl = Some(max_ttl_duration.0); } } } info } } 07070100000024000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001A00000000vault-sync-0.9.2/vault-rs07070100000025000081A4000000000000000000000001665A0E0D00000012000000000000000000000000000000000000002500000000vault-sync-0.9.2/vault-rs/.gitignoretarget Cargo.lock 07070100000026000081A4000000000000000000000001665A0E0D000002E6000000000000000000000000000000000000002500000000vault-sync-0.9.2/vault-rs/Cargo.toml[package] name = "hashicorp_vault" version = "2.1.1" edition = "2018" authors = [ "Chris MacNaughton <chmacnaughton@gmail.com>", "Christopher Brickley <brickley@gmail.com>" ] description = "HashiCorp Vault API client for Rust" license = "MIT" repository = "https://github.com/chrismacnaughton/vault-rs" [features] default = ["native-tls"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] [dependencies] base64 = "~0.13" chrono = "~0.4" serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_json = "1.0" reqwest = { version = "~0.11", default-features = false, features = ["blocking"] } log = "^0.4" quick-error = "~2.0" url = "^2.3" [dependencies.clippy] optional = true version = "^0.0" 07070100000027000081A4000000000000000000000001665A0E0D000000E6000000000000000000000000000000000000002400000000vault-sync-0.9.2/vault-rs/README.mdThis directory contains a fork of https://github.com/ChrisMacNaughton/vault-rs. The original project support only KV v1, and the fork contains fast and dirty changes to support KV v1 and v2. PR to the upstream project to follow. 07070100000028000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000001E00000000vault-sync-0.9.2/vault-rs/src07070100000029000041ED000000000000000000000002665A0E0D00000000000000000000000000000000000000000000002500000000vault-sync-0.9.2/vault-rs/src/client0707010000002A000081A4000000000000000000000001665A0E0D0000056D000000000000000000000000000000000000002E00000000vault-sync-0.9.2/vault-rs/src/client/error.rs/// `Result` type-alias pub type Result<T> = ::std::result::Result<T, Error>; quick_error! { /// Error enum for vault-rs #[derive(Debug)] pub enum Error { /// `reqwest::Error` errors Reqwest(err: ::reqwest::Error) { from() display("reqwest error: {}", err) source(err) } /// `serde_json::Error` SerdeJson(err: ::serde_json::Error) { from() display("serde_json Error: {}", err) source(err) } /// Vault errors Vault(err: String) { display("vault error: {}", err) } /// Response from Vault errors /// This is for when the response is not successful. VaultResponse(err: String, response: reqwest::blocking::Response) { display("Error in vault response: {}", err) } /// IO errors Io(err: ::std::io::Error) { from() display("io error: {}", err) source(err) } /// `Url` parsing error Url(err: ::url::ParseError) { from() display("url parse error: {}", err) source(err) } /// `Base64` decode error Base64(err: ::base64::DecodeError) { from() display("base64 decode error: {}", err) source(err) } } } 0707010000002B000081A4000000000000000000000001665A0E0D0000E5A4000000000000000000000000000000000000002C00000000vault-sync-0.9.2/vault-rs/src/client/mod.rsuse std::collections::HashMap; use std::fmt; use std::io::Read; use std::num::NonZeroU64; use std::result::Result as StdResult; use std::str::FromStr; use crate::client::error::{Error, Result}; use base64; use reqwest::{ self, blocking::{Client, Response}, header::CONTENT_TYPE, Method, }; use serde::de::{self, DeserializeOwned, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::TryInto; use chrono::{DateTime, FixedOffset, NaiveDateTime}; use serde_json; use std::time::Duration; use url::Url; /// Errors pub mod error; /// Secrets engine. /// /// See https://developer.hashicorp.com/vault/api-docs/secret/kv. #[derive(Debug, PartialEq, Eq)] pub enum SecretsEngine { /// KV secrets engine, version 1. /// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v1 KVV1, /// KV secrets engine, version 2. /// https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2 KVV2, } /// Lease duration. /// /// Note: Value returned from vault api is assumed to be in seconds. /// /// ``` /// use hashicorp_vault::client::VaultDuration; /// /// assert_eq!(VaultDuration::days(1), /// VaultDuration(std::time::Duration::from_secs(86400))); /// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct VaultDuration(pub Duration); impl VaultDuration { /// Construct a duration from some number of seconds. pub fn seconds(s: u64) -> VaultDuration { VaultDuration(Duration::from_secs(s)) } /// Construct a duration from some number of minutes. pub fn minutes(m: u64) -> VaultDuration { VaultDuration::seconds(m * 60) } /// Construct a duration from some number of hours. pub fn hours(h: u64) -> VaultDuration { VaultDuration::minutes(h * 60) } /// Construct a duration from some number of days. pub fn days(d: u64) -> VaultDuration { VaultDuration::hours(d * 24) } } impl Serialize for VaultDuration { fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: Serializer, { serializer.serialize_u64(self.0.as_secs()) } } struct VaultDurationVisitor; impl<'de> Visitor<'de> for VaultDurationVisitor { type Value = VaultDuration; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a positive integer") } fn visit_u64<E>(self, value: u64) -> StdResult<Self::Value, E> where E: de::Error, { Ok(VaultDuration(Duration::from_secs(value))) } } impl<'de> Deserialize<'de> for VaultDuration { fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_u64(VaultDurationVisitor) } } /// Number of uses to be used with tokens. /// /// Note: Value returned from vault api can be 0 which means unlimited. /// /// ``` /// use hashicorp_vault::client::VaultNumUses; /// use std::num::NonZeroU64; /// /// let num_uses: VaultNumUses = 10.into(); /// /// match num_uses { /// VaultNumUses::Limited(uses) => assert_eq!(uses.get(), 10), /// VaultNumUses::Unlimited => panic!("Uses shouldn't be unlimited!"), /// } /// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum VaultNumUses { /// The number of uses is unlimited Unlimited, /// The number of uses is limited to the value /// specified that is guaranteed to be non zero. Limited(NonZeroU64), } impl Default for VaultNumUses { fn default() -> Self { VaultNumUses::Unlimited } } impl From<u64> for VaultNumUses { fn from(v: u64) -> Self { match NonZeroU64::new(v) { Some(non_zero) => VaultNumUses::Limited(non_zero), None => VaultNumUses::Unlimited, } } } impl Serialize for VaultNumUses { fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: Serializer, { match self { VaultNumUses::Unlimited => serializer.serialize_u64(0), VaultNumUses::Limited(val) => serializer.serialize_u64(val.clone().into()), } } } struct VaultNumUsesVisitor; impl<'de> Visitor<'de> for VaultNumUsesVisitor { type Value = VaultNumUses; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a positive integer") } fn visit_u64<E>(self, value: u64) -> StdResult<Self::Value, E> where E: de::Error, { Ok(value.into()) } } impl<'de> Deserialize<'de> for VaultNumUses { fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_u64(VaultNumUsesVisitor) } } /// Used for vault responses that return seconds since unix epoch /// See: https://github.com/hashicorp/vault/issues/1654 #[derive(Debug)] pub struct VaultNaiveDateTime(pub NaiveDateTime); struct VaultNaiveDateTimeVisitor; impl<'de> Visitor<'de> for VaultNaiveDateTimeVisitor { type Value = VaultNaiveDateTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a positive integer") } fn visit_u64<E>(self, value: u64) -> StdResult<Self::Value, E> where E: de::Error, { let date_time = NaiveDateTime::from_timestamp_opt(value as i64, 0); match date_time { Some(dt) => Ok(VaultNaiveDateTime(dt)), None => Err(E::custom(format!( "Could not parse: `{}` as a unix timestamp", value, ))), } } } impl<'de> Deserialize<'de> for VaultNaiveDateTime { fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_u64(VaultNaiveDateTimeVisitor) } } /// Used for responses that return RFC 3339 timestamps /// See: https://github.com/hashicorp/vault/issues/1654 #[derive(Debug)] pub struct VaultDateTime(pub DateTime<FixedOffset>); struct VaultDateTimeVisitor; impl<'de> Visitor<'de> for VaultDateTimeVisitor { type Value = VaultDateTime; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a timestamp string") } fn visit_str<E>(self, value: &str) -> StdResult<Self::Value, E> where E: de::Error, { let date_time = DateTime::parse_from_rfc3339(value); match date_time { Ok(dt) => Ok(VaultDateTime(dt)), Err(e) => Err(E::custom(format!( "Could not parse: `{}` as an RFC 3339 timestamp. Error: \ `{:?}`", value, e ))), } } } impl<'de> Deserialize<'de> for VaultDateTime { fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error> where D: Deserializer<'de>, { deserializer.deserialize_str(VaultDateTimeVisitor) } } /// Vault client used to make API requests to the vault #[derive(Debug)] pub struct VaultClient<T> { /// URL to vault instance pub host: Url, /// Token to access vault pub token: String, /// `reqwest::Client` client: Client, /// Data pub data: Option<VaultResponse<T>>, /// The secret backend name. Defaults to 'secret' secret_backend: String, /// Secrets engine secrets_engine: SecretsEngine, } /// Token data, used in `VaultResponse` #[derive(Deserialize, Debug)] pub struct TokenData { /// Accessor token pub accessor: Option<String>, /// Creation time pub creation_time: VaultNaiveDateTime, /// Creation time-to-live pub creation_ttl: Option<VaultDuration>, /// Display name pub display_name: String, /// Max time-to-live pub explicit_max_ttl: Option<VaultDuration>, /// Token id pub id: String, /// Last renewal time pub last_renewal_time: Option<VaultDuration>, /// Meta pub meta: Option<HashMap<String, String>>, /// Number of uses (0: unlimited) pub num_uses: VaultNumUses, /// true if token is an orphan pub orphan: bool, /// Path pub path: String, /// Policies for token pub policies: Vec<String>, /// True if renewable pub renewable: Option<bool>, /// Role pub role: Option<String>, /// Time-to-live pub ttl: VaultDuration, } /// Secret data, used in `VaultResponse` /// /// This struct should onlly ever be necessary for advanced users who /// are creating and parsing Vault responses manually. #[derive(Deserialize, Serialize, Debug)] pub struct SecretDataWrapper<D> { /// data is an opaque data type that holds the response from Vault. pub data: D, } /// Actual Secret data, used in `VaultResponse` #[derive(Deserialize, Serialize, Debug)] struct SecretData { value: String, } /// Transit decrypted data, used in `VaultResponse` #[derive(Deserialize, Serialize, Debug)] struct TransitDecryptedData { plaintext: String, } /// Transit encrypted data, used in `VaultResponse` #[derive(Deserialize, Serialize, Debug)] struct TransitEncryptedData { ciphertext: String, } /// Vault auth #[derive(Deserialize, Debug)] pub struct Auth { /// Client token id pub client_token: String, /// Accessor pub accessor: Option<String>, /// Policies pub policies: Vec<String>, /// Metadata pub metadata: Option<HashMap<String, String>>, /// Lease duration pub lease_duration: Option<VaultDuration>, /// True if renewable pub renewable: bool, } /// Vault response. Different vault responses have different `data` types, so `D` is used to /// represent this. #[derive(Deserialize, Debug)] pub struct VaultResponse<D> { /// Request id pub request_id: String, /// Lease id pub lease_id: Option<String>, /// True if renewable pub renewable: Option<bool>, /// Lease duration (in seconds) pub lease_duration: Option<VaultDuration>, /// Data pub data: Option<D>, /// Warnings pub warnings: Option<Vec<String>>, /// Auth pub auth: Option<Auth>, /// Wrap info, containing token to perform unwrapping pub wrap_info: Option<WrapInfo>, } impl<D> From<VaultResponse<SecretDataWrapper<D>>> for VaultResponse<D> { fn from(v: VaultResponse<SecretDataWrapper<D>>) -> Self { Self { request_id: v.request_id, lease_id: v.lease_id, renewable: v.renewable, lease_duration: v.lease_duration, data: v.data.map(|value| value.data), warnings: v.warnings, auth: v.auth, wrap_info: v.wrap_info, } } } /// Information provided to retrieve a wrapped response #[derive(Deserialize, Debug)] pub struct WrapInfo { /// Time-to-live pub ttl: VaultDuration, /// Token pub token: String, /// Creation time, note this returned in RFC 3339 format pub creation_time: VaultDateTime, /// Wrapped accessor pub wrapped_accessor: Option<String>, } /// Wrapped response is serialized json #[derive(Deserialize, Serialize, Debug)] pub struct WrapData { /// Serialized json string of type `VaultResponse<HashMap<String, String>>` response: String, } /// Token Types #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum TokenType { /// Batch tokens are encrypted blobs that carry enough information /// for them to be used for Vault actions, but they require no /// storage on disk to track them. Batch, /// Service tokens are what users will generally think of as /// "normal" Vault tokens. Service, /// Will use the mount's tuned default. Default, /// For a token store this will default to batch, unless the client requests /// a different type at generation time. DefaultBatch, /// For a token store this will default to service, unless the client requests /// a different type at generation time. DefaultService, } /// `AppRole` properties #[derive(Deserialize, Debug)] pub struct AppRoleProperties { /// Require `secret_id` to be presented when logging in using this `AppRole`. Defaults to 'true'. pub bind_secret_id: bool, /// The secret IDs generated using this role will be cluster local. pub local_secret_ids: bool, /// List of CIDR blocks; if set, specifies blocks of IP addresses which can /// perform the login operation. pub secret_id_bound_cidrs: Option<Vec<String>>, /// Number of times any particular `SecretID` can be used to fetch a token from this `AppRole`, /// after which the `SecretID` will expire. pub secret_id_num_uses: VaultNumUses, /// Duration in either an integer number of seconds (3600) or an integer time unit (60m) after which any SecretID expires. pub secret_id_ttl: VaultDuration, /// List of CIDR blocks; if set, specifies blocks of IP addresses which can authenticate successfully, /// and ties the resulting token to these blocks as well. pub token_bound_cidrs: Option<Vec<String>>, /// If set, will encode an explicit max TTL onto the token. This is a hard cap even if token_ttl and /// token_max_ttl would otherwise allow a renewal. pub token_explicit_max_ttl: Option<VaultDuration>, /// If set, the default policy will not be set on generated tokens; otherwise it will be added to /// the policies set in token_policies. pub token_no_default_policy: Option<bool>, /// Duration after which the issued token can no longer be renewed. pub token_max_ttl: VaultDuration, /// The maximum number of times a generated token may be used (within its lifetime). pub token_num_uses: VaultNumUses, /// The incremental lifetime for generated tokens. /// If set, the token generated using this `AppRole` is a periodic token; so long as it is /// renewed it never expires, but the TTL set on the token at each renewal is fixed to the value /// specified here. If this value is modified, the token will pick up the new value at its next /// renewal. pub token_period: Option<VaultDuration>, /// List of policies to encode onto generated tokens. pub token_policies: Option<Vec<String>>, /// The incremental lifetime for generated tokens. pub token_ttl: VaultDuration, /// The type of token that should be generated. Can be service, batch, or default to use the mount's /// tuned default (which unless changed will be service tokens). For token store roles, there are two /// additional possibilities: default-service and default-batch which specify the type to return unless /// the client requests a different type at generation time. pub token_type: TokenType, } /// Payload to send to vault when authenticating via `AppId` #[derive(Deserialize, Serialize, Debug)] struct AppIdPayload { app_id: String, user_id: String, } /// Payload to send to vault when authenticating via `AppRole` #[derive(Deserialize, Serialize, Debug)] struct AppRolePayload { role_id: String, secret_id: Option<String>, } /// Postgresql secret backend #[derive(Deserialize, Serialize, Debug)] pub struct PostgresqlLogin { /// Password pub password: String, /// Username pub username: String, } /// Response sent by vault when listing policies. We hide this from the /// caller. #[derive(Deserialize, Serialize, Debug)] struct PoliciesResponse { policies: Vec<String>, } /// Response sent by vault when issuing a `LIST` request. #[derive(Deserialize, Serialize, Debug)] pub struct ListResponse { /// keys will include the items listed pub keys: Vec<String>, } /// Options that we use when renewing tokens. #[derive(Deserialize, Serialize, Debug)] struct RenewTokenOptions { /// Token to renew. This can be part of the URL or the body. token: String, /// The amount of time for which to renew the lease. May be ignored or /// overriden by vault. increment: Option<u64>, } /// Options that we use when renewing leases. #[derive(Deserialize, Serialize, Debug)] struct RenewLeaseOptions { lease_id: String, /// The amount of time for which to renew the lease. May be ignored or /// overriden by vault. increment: Option<u64>, } /// Options for creating a token. This is intended to be used as a /// "builder"-style interface, where you create a new `TokenOptions` /// object, call a bunch of chained methods on it, and then pass the result /// to `Client::create_token`. /// /// ``` /// use hashicorp_vault::client::{TokenOptions, VaultDuration}; /// /// let _ = TokenOptions::default() /// .id("test12345") /// .policies(vec!("root")) /// .default_policy(false) /// .orphan(true) /// .renewable(false) /// .display_name("jdoe-temp") /// .number_of_uses(10) /// .ttl(VaultDuration::hours(3)) /// .explicit_max_ttl(VaultDuration::hours(13)); /// ``` /// /// If an option is not specified, it will be set according to [Vault's /// standard defaults for newly-created tokens][token]. /// /// [token]: https://www.vaultproject.io/docs/auth/token.html #[derive(Default, Serialize, Debug)] pub struct TokenOptions { id: Option<String>, policies: Option<Vec<String>>, // TODO: `meta` no_parent: Option<bool>, no_default_policy: Option<bool>, renewable: Option<bool>, ttl: Option<String>, explicit_max_ttl: Option<String>, display_name: Option<String>, num_uses: VaultNumUses, } impl TokenOptions { /// Set the `id` of the created token to the specified value. **This /// may make it easy for attackers to guess your token.** Typically, /// this is used for testing and similar purposes. pub fn id<S: Into<String>>(mut self, id: S) -> Self { self.id = Some(id.into()); self } /// Supply a list of policies that will be used to grant permissions to /// the created token. Unless you also call `default_policy(false)`, the /// policy `default` will be added to this list in modern versions of /// vault. pub fn policies<I>(mut self, policies: I) -> Self where I: IntoIterator, I::Item: Into<String>, { self.policies = Some(policies.into_iter().map(|p| p.into()).collect()); self } /// Should we grant access to the `default` policy? Defaults to true. pub fn default_policy(mut self, enable: bool) -> Self { self.no_default_policy = Some(!enable); self } /// Should this token be an "orphan", allowing it to survive even when /// the token that created it expires or is revoked? pub fn orphan(mut self, orphan: bool) -> Self { self.no_parent = Some(!orphan); self } /// Should the token be renewable? pub fn renewable(mut self, renewable: bool) -> Self { self.renewable = Some(renewable); self } /// For various logging purposes, what should this token be called? pub fn display_name<S>(mut self, name: S) -> Self where S: Into<String>, { self.display_name = Some(name.into()); self } /// How many times can this token be used before it stops working? pub fn number_of_uses<D: Into<VaultNumUses>>(mut self, uses: D) -> Self { self.num_uses = uses.into(); self } /// How long should this token remain valid for? pub fn ttl<D: Into<VaultDuration>>(mut self, ttl: D) -> Self { self.ttl = Some(format!("{}s", ttl.into().0.as_secs())); self } /// How long should this token remain valid for, even if it is renewed /// repeatedly? pub fn explicit_max_ttl<D: Into<VaultDuration>>(mut self, ttl: D) -> Self { self.explicit_max_ttl = Some(format!("{}s", ttl.into().0.as_secs())); self } } /// http verbs #[derive(Debug)] pub enum HttpVerb { /// GET GET, /// POST POST, /// PUT PUT, /// DELETE DELETE, /// LIST LIST, } #[derive(Debug, Serialize)] struct SecretContainer<T: Serialize> { data: T, } #[derive(Debug, Deserialize, Serialize)] struct DefaultSecretType<T: AsRef<str>> { value: T, } /// endpoint response variants #[derive(Debug)] pub enum EndpointResponse<D> { /// Vault response VaultResponse(VaultResponse<D>), /// Empty, but still successful response Empty, } impl VaultClient<TokenData> { /// Construct a `VaultClient` from an existing vault token pub fn new<U, T: Into<String>>(host: U, token: T) -> Result<VaultClient<TokenData>> where U: TryInto<Url, Err = Error>, { let host = host.try_into()?; let client = Client::new(); let token = token.into(); let res = handle_reqwest_response( client .get(host.join("/v1/auth/token/lookup-self")?) .header("X-Vault-Token", token.clone()) .send(), )?; let decoded: VaultResponse<TokenData> = parse_vault_response(res)?; Ok(VaultClient { host, token, client, data: Some(decoded), secret_backend: "secret".into(), secrets_engine: SecretsEngine::KVV2, }) } /// Construct a `VaultClient` from an existing vault token and reqwest::Client pub fn new_from_reqwest<U, T: Into<String>>( host: U, token: T, cli: Client, ) -> Result<VaultClient<TokenData>> where U: TryInto<Url, Err = Error>, { let host = host.try_into()?; let client = cli; let token = token.into(); let res = handle_reqwest_response( client .get(host.join("/v1/auth/token/lookup-self")?) .header("X-Vault-Token", token.clone()) .send(), )?; let decoded: VaultResponse<TokenData> = parse_vault_response(res)?; Ok(VaultClient { host, token, client, data: Some(decoded), secret_backend: "secret".into(), secrets_engine: SecretsEngine::KVV2, }) } } impl VaultClient<()> { /// Construct a `VaultClient` via the `App ID` /// [auth backend](https://www.vaultproject.io/docs/auth/app-id.html) /// /// NOTE: This backend is now deprecated by vault. #[deprecated(since = "0.6.1")] pub fn new_app_id<U, S1: Into<String>, S2: Into<String>>( host: U, app_id: S1, user_id: S2, ) -> Result<VaultClient<()>> where U: TryInto<Url, Err = Error>, { let host = host.try_into()?; let client = Client::new(); let payload = serde_json::to_string(&AppIdPayload { app_id: app_id.into(), user_id: user_id.into(), })?; let res = handle_reqwest_response( client .post(host.join("/v1/auth/app-id/login")?) .body(payload) .send(), )?; let decoded: VaultResponse<()> = parse_vault_response(res)?; let token = match decoded.auth { Some(ref auth) => auth.client_token.clone(), None => { return Err(Error::Vault(format!( "No client token found in response: `{:?}`", &decoded.auth ))) } }; Ok(VaultClient { host, token, client, data: Some(decoded), secret_backend: "secret".into(), secrets_engine: SecretsEngine::KVV2, }) } /// Construct a `VaultClient` via the `AppRole` /// [auth backend](https://www.vaultproject.io/docs/auth/approle.html) pub fn new_app_role<U, R, S>( host: U, role_id: R, secret_id: Option<S>, ) -> Result<VaultClient<()>> where U: TryInto<Url, Err = Error>, R: Into<String>, S: Into<String>, { let host = host.try_into()?; let client = Client::new(); let secret_id = match secret_id { Some(s) => Some(s.into()), None => None, }; let payload = serde_json::to_string(&AppRolePayload { role_id: role_id.into(), secret_id, })?; let res = handle_reqwest_response( client .post(host.join("/v1/auth/approle/login")?) .body(payload) .send(), )?; let decoded: VaultResponse<()> = parse_vault_response(res)?; let token = match decoded.auth { Some(ref auth) => auth.client_token.clone(), None => { return Err(Error::Vault(format!( "No client token found in response: `{:?}`", &decoded.auth ))) } }; Ok(VaultClient { host, token, client, data: Some(decoded), secret_backend: "secret".into(), secrets_engine: SecretsEngine::KVV2, }) } /// Construct a `VaultClient` where no lookup is done through vault since it is assumed that the /// provided token is a single-use token. /// /// A common use case for this method is when a `wrapping_token` has been received and you want /// to query the `sys/wrapping/unwrap` endpoint. pub fn new_no_lookup<U, S: Into<String>>(host: U, token: S) -> Result<VaultClient<()>> where U: TryInto<Url, Err = Error>, { let client = Client::new(); let host = host.try_into()?; Ok(VaultClient { host, token: token.into(), client, data: None, secret_backend: "secret".into(), secrets_engine: SecretsEngine::KVV2, }) } } impl<T> VaultClient<T> where T: DeserializeOwned, { /// Set the backend name to be used by this VaultClient /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let mut client = Client::new(host, token).unwrap(); /// client.secret_backend("my_secrets"); /// ``` pub fn secret_backend<S1: Into<String>>(&mut self, backend_name: S1) { self.secret_backend = backend_name.into(); } /// Set the secrets engine to be used by this VaultClient pub fn secrets_engine(&mut self, secrets_engine: SecretsEngine) { self.secrets_engine = secrets_engine } /// Renew lease for `VaultClient`'s token and updates the /// `self.data.auth` based upon the response. Corresponds to /// [`/auth/token/renew-self`][token]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let mut client = Client::new(host, token).unwrap(); /// /// client.renew().unwrap(); /// ``` /// /// [token]: https://www.vaultproject.io/docs/auth/token.html pub fn renew(&mut self) -> Result<()> { let res = self.post::<_, String>("/v1/auth/token/renew-self", None, None)?; let vault_res: VaultResponse<T> = parse_vault_response(res)?; if let Some(ref mut data) = self.data { data.auth = vault_res.auth; } Ok(()) } /// Renew the lease for the specified token. Requires `root` /// privileges. Corresponds to [`/auth/token/renew[/token]`][token]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// let token_to_renew = "test12345"; /// client.renew_token(token_to_renew, None).unwrap(); /// ``` /// /// [token]: https://www.vaultproject.io/docs/auth/token.html pub fn renew_token<S: Into<String>>(&self, token: S, increment: Option<u64>) -> Result<Auth> { let body = serde_json::to_string(&RenewTokenOptions { token: token.into(), increment, })?; let res = self.post::<_, String>("/v1/auth/token/renew", Some(&body), None)?; let vault_res: VaultResponse<()> = parse_vault_response(res)?; vault_res .auth .ok_or_else(|| Error::Vault("No auth data returned while renewing token".to_owned())) } /// Revoke `VaultClient`'s token. This token can no longer be used. /// Corresponds to [`/auth/token/revoke-self`][token]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::{client, Client}; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// // Create a temporary token, and use it to create a new client. /// let opts = client::TokenOptions::default() /// .ttl(client::VaultDuration::minutes(5)); /// let res = client.create_token(&opts).unwrap(); /// let mut new_client = Client::new(host, res.client_token).unwrap(); /// /// // Issue and use a bunch of temporary dynamic credentials. /// /// // Revoke all our dynamic credentials with a single command. /// new_client.revoke().unwrap(); /// ``` /// /// Note that we consume our `self` parameter, so you cannot use the /// client after revoking it. /// /// [token]: https://www.vaultproject.io/docs/auth/token.html pub fn revoke(self) -> Result<()> { let _ = self.post::<_, String>("/v1/auth/token/revoke-self", None, None)?; Ok(()) } /// Renew a specific lease that your token controls. Corresponds to /// [`/v1/sys/lease`][renew]. /// /// ```no_run /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// use serde::Deserialize; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// #[derive(Deserialize)] /// struct PacketKey { /// api_key_token: String, /// } /// /// let res = client.get_secret_engine_creds::<PacketKey>("packet", "1h-read-only-user").unwrap(); /// /// client.renew_lease(res.lease_id.unwrap(), None).unwrap(); /// ``` /// /// [renew]: https://www.vaultproject.io/docs/http/sys-renew.html pub fn renew_lease<S: Into<String>>( &self, lease_id: S, increment: Option<u64>, ) -> Result<VaultResponse<()>> { let body = serde_json::to_string(&RenewLeaseOptions { lease_id: lease_id.into(), increment, })?; let res = self.put::<_, String>("/v1/sys/leases/renew", Some(&body), None)?; let vault_res: VaultResponse<()> = parse_vault_response(res)?; Ok(vault_res) } /// Lookup token information for this client's token. Corresponds to /// [`/auth/token/lookup-self`][token]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// let res = client.lookup().unwrap(); /// assert!(res.data.unwrap().policies.len() >= 0); /// ``` /// /// [token]: https://www.vaultproject.io/docs/auth/token.html pub fn lookup(&self) -> Result<VaultResponse<TokenData>> { let res = self.get::<_, String>("/v1/auth/token/lookup-self", None)?; let vault_res: VaultResponse<TokenData> = parse_vault_response(res)?; Ok(vault_res) } /// Create a new vault token using the specified options. Corresponds to /// [`/auth/token/create`][token]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::{client, Client}; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// let opts = client::TokenOptions::default() /// .display_name("test_token") /// .policies(vec!("root")) /// .default_policy(false) /// .orphan(true) /// .renewable(false) /// .display_name("jdoe-temp") /// .number_of_uses(10) /// .ttl(client::VaultDuration::minutes(1)) /// .explicit_max_ttl(client::VaultDuration::minutes(3)); /// let res = client.create_token(&opts).unwrap(); /// /// # let new_client = Client::new(host, res.client_token).unwrap(); /// # new_client.revoke().unwrap(); /// ``` /// /// [token]: https://www.vaultproject.io/docs/auth/token.html pub fn create_token(&self, opts: &TokenOptions) -> Result<Auth> { let body = serde_json::to_string(opts)?; let res = self.post::<_, String>("/v1/auth/token/create", Some(&body), None)?; let vault_res: VaultResponse<()> = parse_vault_response(res)?; vault_res .auth .ok_or_else(|| Error::Vault("Created token did not include auth data".into())) } /// /// Saves a secret /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.set_secret("hello_set", "world"); /// assert!(res.is_ok()); /// ``` pub fn set_secret<S1: Into<String>, S2: AsRef<str>>(&self, key: S1, value: S2) -> Result<()> { let secret = DefaultSecretType { value: value.as_ref(), }; self.set_custom_secret(key, &secret) } /// Saves a secret /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// use serde::{Deserialize, Serialize}; /// /// #[derive(Deserialize, Serialize)] /// struct MyThing { /// awesome: String, /// thing: String, /// } /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let secret = MyThing { /// awesome: "I really am cool".into(), /// thing: "this is also in the secret".into(), /// }; /// let res = client.set_custom_secret("hello_set", &secret); /// assert!(res.is_ok()); /// ``` pub fn set_custom_secret<S1, S2>(&self, secret_name: S1, secret: &S2) -> Result<()> where S1: Into<String>, S2: Serialize, { let endpoint = match self.secrets_engine { SecretsEngine::KVV1 => format!("/v1/{}/{}", self.secret_backend, secret_name.into()), SecretsEngine::KVV2 => format!("/v1/{}/data/{}", self.secret_backend, secret_name.into()), }; let json = match self.secrets_engine { SecretsEngine::KVV1 => { serde_json::to_string(&secret)? }, SecretsEngine::KVV2 => { serde_json::to_string(&SecretContainer { data: secret })? }, }; let _ = self.put::<_, String>(&endpoint, Some(&json), None)?; Ok(()) } /// /// List secrets at specified path /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.set_secret("hello/fred", "world"); /// assert!(res.is_ok()); /// let res = client.set_secret("hello/bob", "world"); /// assert!(res.is_ok()); /// let res = client.list_secrets("hello/"); /// assert!(res.is_ok()); /// assert_eq!(res.unwrap(), ["bob", "fred"]); /// ``` pub fn list_secrets<S: AsRef<str>>(&self, key: S) -> Result<Vec<String>> { let endpoint = match self.secrets_engine { SecretsEngine::KVV1 => format!("/v1/{}/{}", self.secret_backend, key.as_ref()), SecretsEngine::KVV2 => format!("/v1/{}/metadata/{}", self.secret_backend, key.as_ref()), }; let res = self.list::<_, String>( &endpoint, None, None, )?; let decoded: VaultResponse<ListResponse> = parse_vault_response(res)?; match decoded.data { Some(data) => Ok(data.keys), _ => Err(Error::Vault(format!( "No secrets found in response: `{:#?}`", decoded ))), } } /// /// Fetches a saved secret /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.set_secret("hello_get", "world"); /// assert!(res.is_ok()); /// let res = client.get_secret("hello_get"); /// assert!(res.is_ok()); /// assert_eq!(res.unwrap(), "world"); /// ``` pub fn get_secret<S: AsRef<str>>(&self, key: S) -> Result<String> { let secret: DefaultSecretType<String> = self.get_custom_secret(key)?; Ok(secret.value) } /// /// Fetches a saved secret /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// use serde::{Deserialize, Serialize}; /// /// #[derive(Debug, Deserialize, Serialize)] /// struct MyThing { /// awesome: String, /// thing: String, /// } /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let secret = MyThing { /// awesome: "I really am cool".into(), /// thing: "this is also in the secret".into(), /// }; /// let res1 = client.set_custom_secret("custom_secret", &secret); /// assert!(res1.is_ok()); /// let res2: Result<MyThing, _> = client.get_custom_secret("custom_secret"); /// assert!(res2.is_ok()); /// let thing = res2.unwrap(); /// assert_eq!(thing.awesome, "I really am cool"); /// assert_eq!(thing.thing, "this is also in the secret"); /// ``` pub fn get_custom_secret<S: AsRef<str>, S2: DeserializeOwned + fmt::Debug>( &self, secret_name: S, ) -> Result<S2> { let endpoint = match self.secrets_engine { SecretsEngine::KVV1 => format!("/v1/{}/{}", self.secret_backend, secret_name.as_ref()), SecretsEngine::KVV2 => format!("/v1/{}/data/{}", self.secret_backend, secret_name.as_ref()), }; let res = self.get::<_, String>(&endpoint, None)?; match self.secrets_engine { SecretsEngine::KVV1 => { let decoded: VaultResponse<S2> = parse_vault_response(res)?; match decoded.data { Some(data) => Ok(data), _ => Err(Error::Vault(format!("No secret found in response: `{:#?}`", decoded))), } }, SecretsEngine::KVV2 => { let decoded: VaultResponse<SecretDataWrapper<S2>> = parse_vault_response(res)?; match decoded.data { Some(data) => Ok(data.data), _ => Err(Error::Vault(format!("No secret found in response: `{:#?}`", decoded))), } }, } } /// Fetch a wrapped secret. Token (one-time use) to fetch secret will be in `wrap_info.token` /// https://www.vaultproject.io/docs/secrets/cubbyhole/index.html pub fn get_secret_wrapped<S1: AsRef<str>, S2: AsRef<str>>( &self, key: S1, wrap_ttl: S2, ) -> Result<VaultResponse<()>> { let res = self.get( &format!("/v1/{}/data/{}", self.secret_backend, key.as_ref())[..], Some(wrap_ttl.as_ref()), )?; parse_vault_response(res) } /// Using a vault client created from a wrapping token, fetch the unwrapped `VaultResponse` from /// `sys/wrapping/unwrap`. /// /// The `data` attribute of `VaultResponse` should contain the unwrapped information, which is /// returned as a `HashMap<String, String>`. pub fn get_unwrapped_response(&self) -> Result<VaultResponse<HashMap<String, String>>> { let res = self.post::<_, String>("/v1/sys/wrapping/unwrap", None, None)?; let result: VaultResponse<SecretDataWrapper<HashMap<String, String>>> = parse_vault_response(res)?; Ok(result.into()) } /// Reads the properties of an existing `AppRole`. pub fn get_app_role_properties<S: AsRef<str>>( &self, role_name: S, ) -> Result<VaultResponse<AppRoleProperties>> { let res = self.get::<_, String>( &format!("/v1/auth/approle/role/{}", role_name.as_ref()), None, )?; parse_vault_response(res) } /// Encrypt a plaintext via Transit secret backend. /// /// # Example /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.transit_encrypt(None, "keyname", b"plaintext"); /// ``` pub fn transit_encrypt<S1: Into<String>, S2: AsRef<[u8]>>( &self, mountpoint: Option<String>, key: S1, plaintext: S2, ) -> Result<Vec<u8>> { let path = mountpoint.unwrap_or_else(|| "transit".to_owned()); let encoded_plaintext = base64::encode(plaintext.as_ref()); let res = self.post::<_, String>( &format!("/v1/{}/encrypt/{}", path, key.into())[..], Some(&format!("{{\"plaintext\": \"{}\"}}", encoded_plaintext)[..]), None, )?; let decoded: VaultResponse<TransitEncryptedData> = parse_vault_response(res)?; let payload = match decoded.data { Some(data) => data.ciphertext, _ => { return Err(Error::Vault(format!( "No ciphertext found in response: `{:#?}`", decoded ))) } }; if !payload.starts_with("vault:v1:") { return Err(Error::Vault(format!( "Unrecognized ciphertext format: `{:#?}`", payload ))); }; let encoded_ciphertext = payload.trim_start_matches("vault:v1:"); let encrypted = base64::decode(encoded_ciphertext)?; Ok(encrypted) } /// Decrypt a ciphertext via Transit secret backend. /// /// # Example /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.transit_decrypt(None, "keyname", b"\x02af\x61bcb\x55d"); /// ``` pub fn transit_decrypt<S1: Into<String>, S2: AsRef<[u8]>>( &self, mountpoint: Option<String>, key: S1, ciphertext: S2, ) -> Result<Vec<u8>> { let path = mountpoint.unwrap_or_else(|| "transit".to_owned()); let encoded_ciphertext = "vault:v1:".to_owned() + &base64::encode(ciphertext.as_ref()); let res = self.post::<_, String>( &format!("/v1/{}/decrypt/{}", path, key.into())[..], Some(&format!("{{\"ciphertext\": \"{}\"}}", encoded_ciphertext)[..]), None, )?; let decoded: VaultResponse<TransitDecryptedData> = parse_vault_response(res)?; let decrypted = match decoded.data { Some(data) => data.plaintext, _ => { return Err(Error::Vault(format!( "No plaintext found in response: `{:#?}`", decoded ))) } }; let plaintext = base64::decode(&decrypted)?; Ok(plaintext) } /// This function is an "escape hatch" of sorts to call any other vault api methods that /// aren't directly supported in this library. /// /// Select the http verb you want, along with the endpoint, e.g. `auth/token/create`, along /// with any wrapping or associated body text and the request will be sent. /// /// See `it_can_perform_approle_workflow` test case for examples. pub fn call_endpoint<D: DeserializeOwned>( &self, http_verb: HttpVerb, endpoint: &str, wrap_ttl: Option<&str>, body: Option<&str>, ) -> Result<EndpointResponse<D>> { let url = format!("/v1/{}", endpoint); match http_verb { HttpVerb::GET => { let mut res = self.get(&url, wrap_ttl)?; parse_endpoint_response(&mut res) } HttpVerb::POST => { let mut res = self.post(&url, body, wrap_ttl)?; parse_endpoint_response(&mut res) } HttpVerb::PUT => { let mut res = self.put(&url, body, wrap_ttl)?; parse_endpoint_response(&mut res) } HttpVerb::DELETE => { let mut res = self.delete(&url)?; parse_endpoint_response(&mut res) } HttpVerb::LIST => { let mut res = self.list(&url, body, wrap_ttl)?; parse_endpoint_response(&mut res) } } } /// Accesses a given endpoint using the provided `wrap_ttl` and returns a single-use /// `wrapping_token` to access the response provided by the endpoint. pub fn get_wrapping_token_for_endpoint( &self, http_verb: HttpVerb, endpoint: &str, wrap_ttl: &str, body: Option<&str>, ) -> Result<String> { let res = self.call_endpoint::<()>(http_verb, endpoint.as_ref(), Some(wrap_ttl), body)?; match res { EndpointResponse::VaultResponse(res) => match res.wrap_info { Some(wrap_info) => Ok(wrap_info.token), _ => Err(Error::Vault(format!( "wrap_info is missing in response: {:?}", res ))), }, EndpointResponse::Empty => Err(Error::Vault("Received an empty response".to_string())), } } /// /// Deletes a saved secret /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let res = client.set_secret("hello_delete", "world"); /// assert!(res.is_ok()); /// let res = client.delete_secret("hello_delete"); /// assert!(res.is_ok()); /// ``` pub fn delete_secret(&self, key: &str) -> Result<()> { let _ = match self.secrets_engine { SecretsEngine::KVV1 => self.delete(&format!("/v1/{}/{}", self.secret_backend, key)[..])?, SecretsEngine::KVV2 => self.delete(&format!("/v1/{}/data/{}", self.secret_backend, key)[..])?, }; Ok(()) } /// Get postgresql secret backend /// https://www.vaultproject.io/docs/secrets/postgresql/index.html pub fn get_postgresql_backend(&self, name: &str) -> Result<VaultResponse<PostgresqlLogin>> { self.get_secret_engine_creds("postgresql", name) } /// Get creds from an arbitrary backend /// ```no_run /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// use serde::Deserialize; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// #[derive(Deserialize)] /// struct PacketKey { /// api_key_token: String, /// } /// /// let res = client.get_secret_engine_creds::<PacketKey>("packet", "1h-read-only-user").unwrap(); /// let api_token = res.data.unwrap().api_key_token; /// ``` pub fn get_secret_engine_creds<K>(&self, backend: &str, name: &str) -> Result<VaultResponse<K>> where K: DeserializeOwned, { let res = self.get::<_, String>(&format!("/v1/{}/creds/{}", backend, name)[..], None)?; let decoded: VaultResponse<K> = parse_vault_response(res)?; Ok(decoded) } /// Get a list of policy names defined by this vault. This requires /// `root` privileges. Corresponds to [`/sys/policy`][/sys/policy]. /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// /// let res = client.policies().unwrap(); /// assert!(res.contains(&"root".to_owned())); /// ``` /// /// [/sys/policy]: https://www.vaultproject.io/docs/http/sys-policy.html pub fn policies(&self) -> Result<Vec<String>> { let res = self.get::<_, String>("/v1/sys/policy", None)?; let decoded: PoliciesResponse = parse_vault_response(res)?; Ok(decoded.policies) } fn get<S1: AsRef<str>, S2: Into<String>>( &self, endpoint: S1, wrap_ttl: Option<S2>, ) -> Result<Response> { let h = self.host.join(endpoint.as_ref())?; match wrap_ttl { Some(wrap_ttl) => Ok(handle_reqwest_response( self.client .request(Method::GET, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .header("X-Vault-Wrap-TTL", wrap_ttl.into()) .send(), )?), None => Ok(handle_reqwest_response( self.client .request(Method::GET, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .send(), )?), } } fn delete<S: AsRef<str>>(&self, endpoint: S) -> Result<Response> { Ok(handle_reqwest_response( self.client .request(Method::DELETE, self.host.join(endpoint.as_ref())?) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .send(), )?) } fn post<S1: AsRef<str>, S2: Into<String>>( &self, endpoint: S1, body: Option<&str>, wrap_ttl: Option<S2>, ) -> Result<Response> { let h = self.host.join(endpoint.as_ref())?; let body = if let Some(body) = body { body.to_string() } else { String::new() }; match wrap_ttl { Some(wrap_ttl) => Ok(handle_reqwest_response( self.client .request(Method::POST, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .header("X-Vault-Wrap-TTL", wrap_ttl.into()) .body(body) .send(), )?), None => Ok(handle_reqwest_response( self.client .request(Method::POST, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .body(body) .send(), )?), } } fn put<S1: AsRef<str>, S2: Into<String>>( &self, endpoint: S1, body: Option<&str>, wrap_ttl: Option<S2>, ) -> Result<Response> { let h = self.host.join(endpoint.as_ref())?; let body = if let Some(body) = body { body.to_string() } else { String::new() }; match wrap_ttl { Some(wrap_ttl) => Ok(handle_reqwest_response( self.client .request(Method::PUT, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .header("X-Vault-Wrap-TTL", wrap_ttl.into()) .body(body) .send(), )?), None => Ok(handle_reqwest_response( self.client .request(Method::PUT, h) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .body(body) .send(), )?), } } fn list<S1: AsRef<str>, S2: Into<String>>( &self, endpoint: S1, body: Option<&str>, wrap_ttl: Option<S2>, ) -> Result<Response> { let h = self.host.join(endpoint.as_ref())?; let body = if let Some(body) = body { body.to_string() } else { String::new() }; match wrap_ttl { Some(wrap_ttl) => Ok(handle_reqwest_response( self.client .request( Method::from_str("LIST".into()).expect("Failed to parse LIST to Method"), h, ) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .header("X-Vault-Wrap-TTL", wrap_ttl.into()) .body(body) .send(), )?), None => Ok(handle_reqwest_response( self.client .request( Method::from_str("LIST".into()).expect("Failed to parse LIST to Method"), h, ) .header("X-Vault-Token", self.token.to_string()) .header(CONTENT_TYPE, "application/json") .body(body) .send(), )?), } } } /// helper fn to check `Response` for success fn handle_reqwest_response(res: StdResult<Response, reqwest::Error>) -> Result<Response> { let mut res = res?; if res.status().is_success() { Ok(res) } else { let mut error_msg = String::new(); let _ = res.read_to_string(&mut error_msg).unwrap_or({ error_msg.push_str("Could not read vault response."); 0 }); Err(Error::VaultResponse( format!( "Vault request failed: {:?}, error message: `{}`", res, error_msg ), res, )) } } /// /// Parse a vault response manually /// /// ``` /// # extern crate hashicorp_vault as vault; /// # use vault::Client; /// use std::io::Read; /// use vault::{Error, Result, client::{VaultResponse, SecretDataWrapper}, TryInto}; /// use std::result::Result as StdResult; /// use reqwest::{ /// blocking::{Client as ReqwestClient, Response}, /// header::CONTENT_TYPE, /// Method, /// }; /// use serde::{Deserialize, Serialize}; /// use url::Url; /// /// #[derive(Debug, Deserialize, Serialize)] /// struct MyThing { /// awesome: String, /// thing: String, /// } /// /// fn handle_reqwest_response(res: StdResult<Response, reqwest::Error>) -> Result<Response> { /// let mut res = res?; /// if res.status().is_success() { /// Ok(res) /// } else { /// let mut error_msg = String::new(); /// let _ = res.read_to_string(&mut error_msg).unwrap_or({ /// error_msg.push_str("Could not read vault response."); /// 0 /// }); /// Err(Error::VaultResponse( /// format!( /// "Vault request failed: {:?}, error message: `{}`", /// res, error_msg /// ), /// res, /// )) /// } /// } /// fn get<S1: AsRef<str>, S2: Into<String>, U: TryInto<Url, Err = Error>>( /// host: U, /// token: &str, /// endpoint: S1, /// wrap_ttl: Option<S2>, /// ) -> Result<Response> { /// let host = host.try_into()?; /// let h = host.join(endpoint.as_ref())?; /// let client = ReqwestClient::new();; /// match wrap_ttl { /// Some(wrap_ttl) => Ok(handle_reqwest_response( /// client /// .request(Method::GET, h) /// .header("X-Vault-Token", token.to_string()) /// .header(CONTENT_TYPE, "application/json") /// .header("X-Vault-Wrap-TTL", wrap_ttl.into()) /// .send(), /// )?), /// None => Ok(handle_reqwest_response( /// client /// .request(Method::GET, h) /// .header("X-Vault-Token", token.to_string()) /// .header(CONTENT_TYPE, "application/json") /// .send(), /// )?), /// } /// } /// let host = "http://127.0.0.1:8200"; /// let token = "test12345"; /// let client = Client::new(host, token).unwrap(); /// let secret = MyThing { /// awesome: "I really am cool".into(), /// thing: "this is also in the secret".into(), /// }; /// let res1 = client.set_custom_secret("custom_secret", &secret); /// assert!(res1.is_ok()); /// let res = get::<&str, &str, &str>( /// host, /// token, /// "/v1/secret/data/custom_secret", /// None, /// ).unwrap(); /// let decoded: VaultResponse<SecretDataWrapper<MyThing>> = vault::client::parse_vault_response(res).unwrap(); /// let res2 = match decoded.data { /// Some(data) => Ok(data.data), /// _ => Err(Error::Vault(format!( /// "No secret found in response: `{:#?}`", /// decoded /// ))), /// }; /// assert!(res2.is_ok()); /// let thing = res2.unwrap(); /// assert_eq!(thing.awesome, "I really am cool"); /// assert_eq!(thing.thing, "this is also in the secret"); pub fn parse_vault_response<T>(res: Response) -> Result<T> where T: DeserializeOwned, { trace!("Response: {:?}", &res); Ok(serde_json::from_reader(res)?) } /// checks if response is empty before attempting to convert to a `VaultResponse` fn parse_endpoint_response<T>(res: &mut Response) -> Result<EndpointResponse<T>> where T: DeserializeOwned, { let mut body = String::new(); let _ = res.read_to_string(&mut body)?; trace!("Response: {:?}", &body); if body.is_empty() { Ok(EndpointResponse::Empty) } else { Ok(EndpointResponse::VaultResponse(serde_json::from_str( &body, )?)) } } 0707010000002C000081A4000000000000000000000001665A0E0D0000369E000000000000000000000000000000000000002500000000vault-sync-0.9.2/vault-rs/src/lib.rs#![deny( missing_docs, missing_debug_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_results )] #![cfg_attr(test, deny(warnings))] #![cfg_attr(feature = "clippy", allow(unstable_features))] #![cfg_attr(feature = "clippy", feature(plugin))] #![cfg_attr(feature = "clippy", plugin(clippy))] #![cfg_attr(feature = "clippy", deny(clippy))] //! Client API for interacting with [Vault](https://www.vaultproject.io/docs/http/index.html) extern crate base64; extern crate reqwest; #[macro_use] extern crate log; #[macro_use] extern crate quick_error; pub extern crate chrono; extern crate serde; pub extern crate url; /// vault client pub mod client; pub use crate::client::error::{Error, Result}; pub use crate::client::VaultClient as Client; use url::Url; /// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417 /// /// An attempted conversion that consumes `self`, which may or may not be expensive. /// /// Library authors should not directly implement this trait, but should prefer implementing /// the [`TryFrom`] trait, which offers greater flexibility and provides an equivalent `TryInto` /// implementation for free, thanks to a blanket implementation in the standard library. /// /// [`TryFrom`]: trait.TryFrom.html pub trait TryInto<T>: Sized { /// The type returned in the event of a conversion error. type Err; /// Performs the conversion. fn try_into(self) -> ::std::result::Result<T, Self::Err>; } /// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417 /// /// Attempt to construct `Self` via a conversion. pub trait TryFrom<T>: Sized { /// The type returned in the event of a conversion error. type Err; /// Performs the conversion. fn try_from(_: T) -> ::std::result::Result<Self, Self::Err>; } impl<T, U> TryInto<U> for T where U: TryFrom<T>, { type Err = U::Err; fn try_into(self) -> ::std::result::Result<U, U::Err> { U::try_from(self) } } impl TryFrom<Url> for Url { type Err = Error; fn try_from(u: Url) -> ::std::result::Result<Self, Self::Err> { Ok(u) } } impl<'a> TryFrom<&'a Url> for Url { type Err = Error; fn try_from(u: &Url) -> ::std::result::Result<Self, Self::Err> { Ok(u.clone()) } } impl<'a> TryFrom<&'a str> for Url { type Err = Error; fn try_from(s: &str) -> ::std::result::Result<Self, Self::Err> { match Url::parse(s) { Ok(u) => Ok(u), Err(e) => Err(e.into()), } } } impl<'a> TryFrom<&'a String> for Url { type Err = Error; fn try_from(s: &String) -> ::std::result::Result<Self, Self::Err> { match Url::parse(s) { Ok(u) => Ok(u), Err(e) => Err(e.into()), } } } impl TryFrom<String> for Url { type Err = Error; fn try_from(s: String) -> ::std::result::Result<Self, Self::Err> { match Url::parse(&s) { Ok(u) => Ok(u), Err(e) => Err(e.into()), } } } #[cfg(test)] mod tests { use crate::client::HttpVerb::*; use crate::client::VaultClient as Client; use crate::client::{self, EndpointResponse}; use crate::Error; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::Value; /// vault host for testing const HOST: &str = "http://127.0.0.1:8200"; /// root token needed for testing const TOKEN: &str = "test12345"; #[test] fn it_can_create_a_client() { let _ = Client::new(HOST, TOKEN).unwrap(); } #[test] fn it_can_create_a_client_from_a_string_reference() { let _ = Client::new(&HOST.to_string(), TOKEN).unwrap(); } #[test] fn it_can_create_a_client_from_a_string() { let _ = Client::new(HOST.to_string(), TOKEN).unwrap(); } #[test] fn it_can_query_secrets() { let client = Client::new(HOST, TOKEN).unwrap(); let res = client.set_secret("hello_query", "world"); assert!(res.is_ok()); let res = client.get_secret("hello_query").unwrap(); assert_eq!(res, "world"); } #[test] fn it_can_store_json_secrets() { let client = Client::new(HOST, TOKEN).unwrap(); let json = "{\"foo\": {\"bar\": [\"baz\"]}}"; let res = client.set_secret("json_secret", json); assert!(res.is_ok()); let res = client.get_secret("json_secret").unwrap(); assert_eq!(res, json) } #[test] fn it_can_list_secrets() { let client = Client::new(HOST, TOKEN).unwrap(); let _res = client.set_secret("hello/fred", "world").unwrap(); // assert!(res.is_ok()); let res = client.set_secret("hello/bob", "world"); assert!(res.is_ok()); let res = client.list_secrets("hello"); assert!(res.is_ok()); assert_eq!(res.unwrap(), ["bob", "fred"]); let res = client.list_secrets("hello/"); assert!(res.is_ok()); assert_eq!(res.unwrap(), ["bob", "fred"]); } #[test] fn it_can_detect_404_status() { let client = Client::new(HOST, TOKEN).unwrap(); let res = client.list_secrets("non/existent/key"); assert!(res.is_err()); if let Err(Error::VaultResponse(_, response)) = res { assert_eq!(response.status(), StatusCode::NOT_FOUND); } else { panic!("Error should match on VaultResponse with reqwest response."); } } #[test] fn it_can_write_secrets_with_newline() { let client = Client::new(HOST, TOKEN).unwrap(); let res = client.set_secret("hello_set", "world\n"); assert!(res.is_ok()); let res = client.get_secret("hello_set").unwrap(); assert_eq!(res, "world\n"); } #[test] fn it_returns_err_on_forbidden() { let client = Client::new(HOST, "test123456"); // assert_eq!(Err("Forbidden".to_string()), client); assert!(client.is_err()); } #[test] fn it_can_delete_a_secret() { let client = Client::new(HOST, TOKEN).unwrap(); let res = client.set_secret("hello_delete", "world"); assert!(res.is_ok()); let res = client.get_secret("hello_delete").unwrap(); assert_eq!(res, "world"); let res = client.delete_secret("hello_delete"); assert!(res.is_ok()); let res = client.get_secret("hello_delete"); assert!(res.is_err()); } #[test] fn it_can_perform_approle_workflow() { use std::collections::HashMap; let c = Client::new(HOST, TOKEN).unwrap(); let mut body = "{\"type\":\"approle\"}"; // Ensure we do not currently have an approle backend enabled. // Older vault versions (<1.2.0) seem to have an AppRole backend // enabled by default, so calling the POST to create a new one // fails with a 400 status let _: EndpointResponse<()> = c .call_endpoint(DELETE, "sys/auth/approle", None, None) .unwrap(); // enable approle auth backend let mut res: EndpointResponse<()> = c .call_endpoint(POST, "sys/auth/approle", None, Some(body)) .unwrap(); panic_non_empty(&res); // make a new approle body = "{\"secret_id_ttl\":\"10m\", \"token_ttl\":\"20m\", \"token_max_ttl\":\"30m\", \ \"secret_id_num_uses\":40}"; res = c .call_endpoint(POST, "auth/approle/role/test_role", None, Some(body)) .unwrap(); panic_non_empty(&res); // let's test the properties endpoint while we're here let _ = c.get_app_role_properties("test_role").unwrap(); // get approle's role-id let res: EndpointResponse<HashMap<String, String>> = c .call_endpoint(GET, "auth/approle/role/test_role/role-id", None, None) .unwrap(); let data = match res { EndpointResponse::VaultResponse(res) => res.data.unwrap(), _ => panic!("expected vault response, got: {:?}", res), }; let role_id = &data["role_id"]; assert!(!role_id.is_empty()); // now get a secret id for this approle let res: EndpointResponse<HashMap<String, Value>> = c .call_endpoint(POST, "auth/approle/role/test_role/secret-id", None, None) .unwrap(); let data = match res { EndpointResponse::VaultResponse(res) => res.data.unwrap(), _ => panic!("expected vault response, got: {:?}", res), }; let secret_id = &data["secret_id"].as_str().unwrap(); // now finally we can try to actually login! let _ = Client::new_app_role(HOST, &role_id[..], Some(&secret_id[..])).unwrap(); // clean up by disabling approle auth backend let res = c .call_endpoint(DELETE, "sys/auth/approle", None, None) .unwrap(); panic_non_empty(&res); } #[test] fn it_can_read_a_wrapped_secret() { let client = Client::new(HOST, TOKEN).unwrap(); let res = client.set_secret("hello_delete_2", "second world"); assert!(res.is_ok()); // wrap the secret's value in `sys/wrapping/unwrap` with a TTL of 2 minutes let res = client.get_secret_wrapped("hello_delete_2", "2m").unwrap(); let wrapping_token = res.wrap_info.unwrap().token; // make a new client with the wrapping token let c2 = Client::new_no_lookup(HOST, wrapping_token).unwrap(); // read the cubbyhole response (can only do this once!) let res = c2.get_unwrapped_response().unwrap(); assert_eq!(res.data.unwrap()["value"], "second world"); } #[test] fn it_can_store_policies() { // use trailing slash for host to ensure Url processing fixes this later let c = Client::new("http://127.0.0.1:8200/", TOKEN).unwrap(); let body = "{\"policy\":\"{}\"}"; // enable approle auth backend let res: EndpointResponse<()> = c .call_endpoint(PUT, "sys/policy/test_policy_1", None, Some(body)) .unwrap(); panic_non_empty(&res); let res: EndpointResponse<()> = c .call_endpoint(PUT, "sys/policy/test_policy_2", None, Some(body)) .unwrap(); panic_non_empty(&res); let client_policies = c.policies().unwrap(); let expected_policies = ["default", "test_policy_1", "test_policy_2", "root"]; let _ = expected_policies .iter() .map(|p| { assert!(client_policies.contains(&(*p).to_string())); }) .last(); let token_name = "policy_test_token".to_string(); let token_opts = client::TokenOptions::default() .policies(vec!["test_policy_1", "test_policy_2"].into_iter()) .default_policy(false) .id(&token_name[..]) .ttl(client::VaultDuration::minutes(1)); let _ = c.create_token(&token_opts).unwrap(); let body = format!("{{\"token\":\"{}\"}}", &token_name); let res: EndpointResponse<client::TokenData> = c .call_endpoint(POST, "auth/token/lookup", None, Some(&body)) .unwrap(); match res { EndpointResponse::VaultResponse(res) => { let data = res.data.unwrap(); let mut policies = data.policies; policies.sort(); assert_eq!(policies, ["test_policy_1", "test_policy_2"]); } _ => panic!("expected vault response, got: {:?}", res), } // clean-up let res: EndpointResponse<()> = c .call_endpoint(DELETE, "sys/policy/test_policy_1", None, None) .unwrap(); panic_non_empty(&res); let res: EndpointResponse<()> = c .call_endpoint(DELETE, "sys/policy/test_policy_2", None, None) .unwrap(); panic_non_empty(&res); } #[test] fn it_can_list_things() { let c = Client::new(HOST, TOKEN).unwrap(); let _ = c .create_token(&client::TokenOptions::default().ttl(client::VaultDuration::minutes(1))) .unwrap(); let res: EndpointResponse<client::ListResponse> = c .call_endpoint(LIST, "auth/token/accessors", None, None) .unwrap(); match res { EndpointResponse::VaultResponse(res) => { let data = res.data.unwrap(); assert!(data.keys.len() > 2); } _ => panic!("expected vault response, got: {:?}", res), } } #[test] fn it_can_encrypt_decrypt_transit() { let key_id = "test-vault-rs"; let plaintext = b"data\0to\0encrypt"; let client = Client::new(HOST, TOKEN).unwrap(); let enc_resp = client.transit_encrypt(None, key_id, plaintext); let encrypted = enc_resp.unwrap(); let dec_resp = client.transit_decrypt(None, key_id, encrypted); let payload = dec_resp.unwrap(); assert_eq!(plaintext, payload.as_slice()); } // helper fn to panic on empty responses fn panic_non_empty(res: &EndpointResponse<()>) { match *res { EndpointResponse::Empty => {} _ => panic!("expected empty response, received: {:?}", res), } } #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] struct CustomSecretType { name: String, } #[test] fn it_can_set_and_get_a_custom_secret_type() { let input = CustomSecretType { name: "test".into(), }; let client = Client::new(HOST, TOKEN).unwrap(); let res = client.set_custom_secret("custom_type", &input); assert!(res.is_ok()); let res: CustomSecretType = client.get_custom_secret("custom_type").unwrap(); assert_eq!(res, input); } } 0707010000002D000081A4000000000000000000000001665A0E0D000009B8000000000000000000000000000000000000002900000000vault-sync-0.9.2/vault-sync.example.yaml# Configuration file for vault-sync # https://github.com/pbchekin/vault-sync # Name for this vault-sync instance. If there are multiple vault-sync instances running for the same # source Vault, then this name must be unique for each instance. id: vault-sync # Time between full syncs. The full sync usually runs when vault-sync starts, then vault-sync only # apply changes for the secrets. However, vault-sync also does the full sync every this interval. # It does not do any changes to the destination, if the source secrets are not changed. full_sync_interval: 3600 # 1h # Optional address and port for this vault-sync to listen for the Vault audit log. Set this if you # are planning to use the Vault audit device. # bind: 0.0.0.0:8202 # Source Vault configuration to sync secrets from. src: # Vault URL url: http://127.0.0.1:8200/ # Prefix for secrets: only secrets with path starting from this prefix will be synchronized with # the target Vault. Use empty string ("") for all secrets. prefix: "" # Path for the secrets engine, default is "secret". # backend: secret # Secrets engine version, default is 2. # version: 2 # Vault Token auth method # Set token (or environment variable VAULT_SYNC_SRC_TOKEN) # token: *** # token_ttl: 86400 # optional, 12h # Vault AppRole auth method # Set role_id and secret_id (or environment variables VAULT_SYNC_SRC_ROLE_ID and VAULT_SYNC_SRC_SECRET_ID) # role_id: *** # secret_id: *** # token_ttl: 86400 # optional, 12h # token_max_ttl: 2764800 # 32d # Destination Vault configuration to sync secrets to. dst: # Vault URL url: http://127.0.0.1:8200/ # Prefix for secrets: this prefix will replace the corresponding prefix from the 'src' section. # This allows syncing a tree of secrets to a non overlapping tree in the same Vault. # For example: if src.prefix is "src" and dst.prefix is "dst", then secret "src/secret1" will be # synced to "dst/secret1". prefix: "" # Path for the secrets engine, default is "secret". # backend: secret # Secrets engine version, default is 2. # version: 2 # Vault Token auth method # Set token (or environment variable VAULT_SYNC_DST_TOKEN) # token: *** # token_ttl: 86400 # optional, 12h # Vault AppRole auth method # Set role_id and secret_id (or environment variables VAULT_SYNC_DST_ROLE_ID and VAULT_SYNC_DST_SECRET_ID) # role_id: *** # secret_id: *** # token_ttl: 86400 # optional, 12h # token_max_ttl: 2764800 # 32d 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!390 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor