├── .dockerignore ├── vault-rs ├── .gitignore ├── README.md ├── Cargo.toml └── src │ ├── client │ └── error.rs │ └── lib.rs ├── .gitignore ├── install └── helm │ └── vault-sync │ ├── templates │ ├── configmap.yaml │ ├── secret.yaml │ ├── serviceaccount.yaml │ ├── service.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ └── deployment.yaml │ ├── .helmignore │ ├── Chart.yaml │ └── values.yaml ├── docker └── Dockerfile ├── Cargo.toml ├── src ├── audit.rs ├── main.rs ├── vault.rs ├── config.rs └── sync.rs ├── .github └── workflows │ ├── docker.yml │ └── ci.yml ├── vault-sync.example.yaml ├── scripts ├── test-helm.sh └── test-sync.sh ├── README.md ├── LICENSE └── Cargo.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | docker/ 3 | -------------------------------------------------------------------------------- /vault-rs/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | vault-sync.yaml 4 | vault*.log 5 | vault*.pid 6 | -------------------------------------------------------------------------------- /vault-rs/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a fork of https://github.com/ChrisMacNaughton/vault-rs. 2 | 3 | The original project support only KV v1, and the fork contains fast and dirty changes to support KV v1 and v2. 4 | PR to the upstream project to follow. 5 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "vault-sync.fullname" . }} 5 | labels: 6 | {{- include "vault-sync.labels" . | nindent 4 }} 7 | data: 8 | vault-sync.yaml: | 9 | {{- toYaml .Values.vaultSync | nindent 4 }} 10 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.existingSecretName }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "vault-sync.fullname" . }} 6 | labels: 7 | {{- include "vault-sync.labels" . | nindent 4 }} 8 | type: Opaque 9 | data: 10 | {{- toYaml .Values.secrets | nindent 2 }} 11 | {{- end }} -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "vault-sync.serviceAccountName" . }} 6 | labels: 7 | {{- include "vault-sync.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.85.0 AS builder 2 | 3 | WORKDIR /usr/src/vault-sync 4 | COPY . . 5 | RUN cargo install --path . 6 | 7 | FROM debian:bookworm-slim 8 | RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/* 9 | COPY --from=builder /usr/local/cargo/bin/vault-sync /usr/local/bin/vault-sync 10 | # Smoke check that the image has all required libraries 11 | RUN /usr/local/bin/vault-sync --version 12 | CMD ["vault-sync"] 13 | -------------------------------------------------------------------------------- /install/helm/vault-sync/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vault-sync" 3 | version = "0.11.0" 4 | authors = ["Pavel Chekin "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | clap = "2.34.0" 9 | ctrlc = { version = "3.2.3", features = ["termination"] } 10 | log = "0.4.17" 11 | serde = { version = "1.0.144", features = ["derive"] } 12 | serde_json = "1.0.107" 13 | serde_repr = "0.1.16" 14 | serde_yaml = "0.9.25" 15 | simplelog = "0.12.0" 16 | 17 | [dependencies.hashicorp_vault] 18 | path = "vault-rs" 19 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.vaultSync.bind }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "vault-sync.fullname" . }} 6 | labels: 7 | {{- include "vault-sync.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "vault-sync.selectorLabels" . | nindent 4 }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /src/audit.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub struct AuditLog { 5 | #[allow(dead_code)] 6 | pub time: String, 7 | #[serde(rename = "type")] 8 | pub log_type: String, 9 | pub request: Request, 10 | } 11 | 12 | #[derive(Deserialize, Debug)] 13 | pub struct Request { 14 | pub operation: String, 15 | pub mount_type: Option, 16 | pub path: String, 17 | } 18 | 19 | #[derive(Serialize, Debug)] 20 | pub struct CreateAuditDeviceRequest { 21 | #[serde(rename = "type")] 22 | pub audit_device_type: String, 23 | pub options: AuditDeviceOptions, 24 | } 25 | 26 | #[derive(Serialize, Debug)] 27 | pub struct AuditDeviceOptions { 28 | pub address: String, 29 | pub socket_type: String, 30 | } 31 | -------------------------------------------------------------------------------- /vault-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hashicorp_vault" 3 | version = "2.1.1" 4 | edition = "2018" 5 | authors = [ 6 | "Chris MacNaughton ", 7 | "Christopher Brickley " 8 | ] 9 | description = "HashiCorp Vault API client for Rust" 10 | license = "MIT" 11 | repository = "https://github.com/chrismacnaughton/vault-rs" 12 | 13 | [features] 14 | default = ["native-tls"] 15 | native-tls = ["reqwest/native-tls"] 16 | rustls-tls = ["reqwest/rustls-tls"] 17 | 18 | [dependencies] 19 | base64 = "~0.13" 20 | chrono = "~0.4" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_derive = "1.0" 23 | serde_json = "1.0" 24 | reqwest = { version = "~0.11", default-features = false, features = ["blocking"] } 25 | log = "^0.4" 26 | quick-error = "~2.0" 27 | url = "^2.3" 28 | 29 | [dependencies.clippy] 30 | optional = true 31 | version = "^0.0" 32 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "vault-sync.fullname" . }} 6 | labels: 7 | {{- include "vault-sync.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "vault-sync.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /install/helm/vault-sync/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: vault-sync 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.6 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 0.11.0 24 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "vault-sync.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | apiVersion: networking.k8s.io/v1beta1 6 | {{- else -}} 7 | apiVersion: extensions/v1beta1 8 | {{- end }} 9 | kind: Ingress 10 | metadata: 11 | name: {{ $fullName }} 12 | labels: 13 | {{- include "vault-sync.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . | quote }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ .host | quote }} 32 | http: 33 | paths: 34 | {{- range .paths }} 35 | - path: {{ . }} 36 | backend: 37 | serviceName: {{ $fullName }} 38 | servicePort: {{ $svcPort }} 39 | {{- end }} 40 | {{- end }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | docker: 12 | name: Docker 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | shell: bash 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set Docker Tag 24 | id: tag 25 | run: | 26 | if [[ $GITHUB_REF == refs/heads/main ]]; then 27 | DOCKER_TAG="main" 28 | else 29 | DOCKER_TAG="$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml)" 30 | fi 31 | echo "tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT 32 | 33 | - name: Build Docker Image 34 | run: docker build -t pbchekin/vault-sync:${{ steps.tag.outputs.tag }} -f docker/Dockerfile . 35 | 36 | - name: Login to Docker Hub 37 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') 38 | run: docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password ${{ secrets.DOCKERHUB_PASSWORD }} 39 | 40 | - name: Push Docker Image 41 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') 42 | run: docker push pbchekin/vault-sync:${{ steps.tag.outputs.tag }} 43 | -------------------------------------------------------------------------------- /vault-rs/src/client/error.rs: -------------------------------------------------------------------------------- 1 | /// `Result` type-alias 2 | pub type Result = ::std::result::Result; 3 | 4 | quick_error! { 5 | /// Error enum for vault-rs 6 | #[derive(Debug)] 7 | pub enum Error { 8 | /// `reqwest::Error` errors 9 | Reqwest(err: ::reqwest::Error) { 10 | from() 11 | display("reqwest error: {}", err) 12 | source(err) 13 | } 14 | /// `serde_json::Error` 15 | SerdeJson(err: ::serde_json::Error) { 16 | from() 17 | display("serde_json Error: {}", err) 18 | source(err) 19 | } 20 | /// Vault errors 21 | Vault(err: String) { 22 | display("vault error: {}", err) 23 | } 24 | /// Response from Vault errors 25 | /// This is for when the response is not successful. 26 | VaultResponse(err: String, response: reqwest::blocking::Response) { 27 | display("Error in vault response: {}", err) 28 | } 29 | /// IO errors 30 | Io(err: ::std::io::Error) { 31 | from() 32 | display("io error: {}", err) 33 | source(err) 34 | } 35 | /// `Url` parsing error 36 | Url(err: ::url::ParseError) { 37 | from() 38 | display("url parse error: {}", err) 39 | source(err) 40 | } 41 | /// `Base64` decode error 42 | Base64(err: ::base64::DecodeError) { 43 | from() 44 | display("base64 decode error: {}", err) 45 | source(err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "vault-sync.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "vault-sync.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "vault-sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | 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}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "vault-sync.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "vault-sync.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "vault-sync.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "vault-sync.labels" -}} 37 | helm.sh/chart: {{ include "vault-sync.chart" . }} 38 | {{ include "vault-sync.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "vault-sync.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "vault-sync.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "vault-sync.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "vault-sync.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /install/helm/vault-sync/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for vault-sync. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # See https://github.com/pbchekin/vault-sync/blob/main/vault-sync.example.yaml 6 | vaultSync: 7 | id: vault-sync 8 | full_sync_interval: 3600 9 | # bind: 0.0.0.0:8202 10 | src: 11 | url: http://127.0.0.1:8200/ 12 | prefix: "" 13 | backend: secret 14 | # token_ttl: 86400 15 | # token_max_ttl: 2764800 16 | dst: 17 | url: http://127.0.0.1:8200/ 18 | prefix: "" 19 | backend: secret 20 | # token_ttl: 86400 21 | # token_max_ttl: 2764800 22 | 23 | existingSecretName: "" 24 | # Secrets must be base64 encoded 25 | secrets: 26 | VAULT_SYNC_SRC_TOKEN: eHh4 27 | # VAULT_SYNC_SRC_ROLE_ID: xxx 28 | # VAULT_SYNC_SRC_SECRET_ID: xxx 29 | VAULT_SYNC_DST_TOKEN: eHh4 30 | # VAULT_SYNC_DST_ROLE_ID: xxx 31 | # VAULT_SYNC_DST_SECRET_ID: xxx 32 | 33 | replicaCount: 1 34 | 35 | image: 36 | repository: pbchekin/vault-sync 37 | pullPolicy: IfNotPresent 38 | # Overrides the image tag whose default is the chart appVersion. 39 | tag: "" 40 | 41 | imagePullSecrets: [] 42 | nameOverride: "" 43 | fullnameOverride: "" 44 | 45 | serviceAccount: 46 | # Specifies whether a service account should be created 47 | create: true 48 | # Annotations to add to the service account 49 | annotations: {} 50 | # The name of the service account to use. 51 | # If not set and create is true, a name is generated using the fullname template 52 | name: "" 53 | 54 | podAnnotations: {} 55 | 56 | podSecurityContext: {} 57 | # fsGroup: 2000 58 | 59 | securityContext: {} 60 | # capabilities: 61 | # drop: 62 | # - ALL 63 | # readOnlyRootFilesystem: true 64 | # runAsNonRoot: true 65 | # runAsUser: 1000 66 | 67 | service: 68 | type: ClusterIP 69 | port: 8202 70 | 71 | ingress: 72 | enabled: false 73 | annotations: {} 74 | # kubernetes.io/ingress.class: nginx 75 | # kubernetes.io/tls-acme: "true" 76 | hosts: 77 | - host: chart-example.local 78 | paths: [] 79 | tls: [] 80 | # - secretName: chart-example-tls 81 | # hosts: 82 | # - chart-example.local 83 | 84 | resources: {} 85 | # We usually recommend not to specify default resources and to leave this as a conscious 86 | # choice for the user. This also increases chances charts run on environments with little 87 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 88 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 89 | # limits: 90 | # cpu: 100m 91 | # memory: 128Mi 92 | # requests: 93 | # cpu: 100m 94 | # memory: 128Mi 95 | 96 | autoscaling: 97 | enabled: false 98 | minReplicas: 1 99 | maxReplicas: 100 100 | targetCPUUtilizationPercentage: 80 101 | # targetMemoryUtilizationPercentage: 80 102 | 103 | nodeSelector: {} 104 | 105 | tolerations: [] 106 | 107 | affinity: {} 108 | 109 | volumes: [] 110 | 111 | volumeMounts: [] 112 | 113 | environmentVars: [] 114 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "5 0 * * 0" 7 | push: 8 | branches: 9 | - main 10 | tags: 11 | - "*" 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | env: 17 | CARGO_TERM_COLOR: always 18 | VAULT_ADDR: http://127.0.0.1:8200 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Run tests 28 | run: cargo test --verbose 29 | 30 | - name: Build 31 | run: cargo build --verbose 32 | 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: vault-sync 36 | path: target/debug/vault-sync 37 | if-no-files-found: error 38 | 39 | test-vault: 40 | runs-on: ubuntu-latest 41 | needs: build 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: actions/download-artifact@v4 47 | with: 48 | name: vault-sync 49 | 50 | - name: Fix permissions 51 | run: | 52 | chmod 0755 ./vault-sync 53 | 54 | - name: Install Vault 55 | run: | 56 | sudo apt-get update -y 57 | sudo apt-get install -y gpg 58 | wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg >/dev/null 59 | gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint 60 | 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 61 | sudo apt-get update -y 62 | sudo apt-get install -y vault 63 | vault version 64 | 65 | - name: Run local test 66 | run: VAULT_SYNC_BINARY=./vault-sync ./scripts/test-sync.sh 67 | 68 | test-bao: 69 | runs-on: ubuntu-latest 70 | needs: build 71 | 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - name: Install OpenBao 76 | env: 77 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | run: | 79 | LATEST_RELEASE=$(gh release list --repo openbao/openbao --json name,isLatest --jq '.[] | select(.isLatest) | .name') 80 | gh release download --repo openbao/openbao $LATEST_RELEASE -p 'bao_*_Linux_x86_64.tar.gz' 81 | tar zxf bao_*.tar.gz bao 82 | ./bao version 83 | 84 | - uses: actions/download-artifact@v4 85 | with: 86 | name: vault-sync 87 | 88 | - name: Fix permissions 89 | run: | 90 | chmod 0755 vault-sync 91 | 92 | - name: Use bao instead of vault 93 | run: | 94 | ln -s bao vault 95 | echo $PWD >> $GITHUB_PATH 96 | 97 | - name: Run local test 98 | run: VAULT_SYNC_BINARY=./vault-sync ./scripts/test-sync.sh --namespaces 99 | 100 | kubernetes: 101 | runs-on: ubuntu-latest 102 | 103 | steps: 104 | - uses: actions/checkout@v4 105 | 106 | - name: Run Helm tests 107 | run: | 108 | ./scripts/test-helm.sh 109 | -------------------------------------------------------------------------------- /install/helm/vault-sync/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "vault-sync.fullname" . }} 5 | labels: 6 | {{- include "vault-sync.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "vault-sync.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "vault-sync.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "vault-sync.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | command: ["vault-sync", "--config", "/config/vault-sync.yaml"] 37 | volumeMounts: 38 | - name: config-volume 39 | mountPath: /config 40 | {{- with .Values.volumeMounts }} 41 | {{- toYaml . | nindent 12 }} 42 | {{- end }} 43 | {{- if .Values.vaultSync.bind }} 44 | ports: 45 | - name: tcp 46 | containerPort: {{ (split ":" .Values.vaultSync.bind)._1 }} 47 | protocol: TCP 48 | {{- end }} 49 | resources: 50 | {{- toYaml .Values.resources | nindent 12 }} 51 | env: 52 | - name: RUST_LOG 53 | value: info' 54 | {{- if not .Values.existingSecretName }} 55 | {{- $secretName := include "vault-sync.fullname" . }} 56 | {{- range $key, $_ := .Values.secrets }} 57 | - name: {{ $key }} 58 | valueFrom: 59 | secretKeyRef: 60 | name: {{ $secretName }} 61 | key: {{ $key }} 62 | {{- end }} 63 | {{- end }} 64 | {{- if .Values.existingSecretName }} 65 | envFrom: 66 | - secretRef: 67 | name: {{ .Values.existingSecretName }} 68 | {{- end }} 69 | {{- with .Values.environmentVars }} 70 | {{- toYaml . | nindent 12 }} 71 | {{- end }} 72 | {{- with .Values.nodeSelector }} 73 | nodeSelector: 74 | {{- toYaml . | nindent 8 }} 75 | {{- end }} 76 | {{- with .Values.affinity }} 77 | affinity: 78 | {{- toYaml . | nindent 8 }} 79 | {{- end }} 80 | {{- with .Values.tolerations }} 81 | tolerations: 82 | {{- toYaml . | nindent 8 }} 83 | {{- end }} 84 | volumes: 85 | - name: config-volume 86 | configMap: 87 | name: {{ include "vault-sync.fullname" . }} 88 | {{- with .Values.volumes }} 89 | {{- toYaml . | nindent 8 }} 90 | {{- end }} 91 | -------------------------------------------------------------------------------- /vault-sync.example.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for vault-sync 2 | # https://github.com/pbchekin/vault-sync 3 | 4 | # Name for this vault-sync instance. If there are multiple vault-sync instances running for the same 5 | # source Vault, then this name must be unique for each instance. 6 | id: vault-sync 7 | 8 | # Time between full syncs. The full sync usually runs when vault-sync starts, then vault-sync only 9 | # apply changes for the secrets. However, vault-sync also does the full sync every this interval. 10 | # It does not do any changes to the destination, if the source secrets are not changed. 11 | full_sync_interval: 3600 # 1h 12 | 13 | # Optional address and port for this vault-sync to listen for the Vault audit log. Set this if you 14 | # are planning to use the Vault audit device. 15 | # bind: 0.0.0.0:8202 16 | 17 | # Source Vault configuration to sync secrets from. 18 | src: 19 | # Vault URL 20 | url: http://127.0.0.1:8200/ 21 | 22 | # Prefix for secrets: only secrets with path starting from this prefix will be synchronized with 23 | # the target Vault. Use empty string ("") for all secrets. 24 | prefix: "" 25 | 26 | # Vault namespace, not set by default. 27 | # namespace: null 28 | 29 | # Path for the secrets engine. For multiple backends use "backends" with a list. 30 | # Default is single backend "secret". 31 | # backend: secret 32 | # or 33 | # backends: 34 | # - secret1 35 | # - secret2 36 | 37 | # Secrets engine version, default is 2. 38 | # version: 2 39 | 40 | # Vault Token auth method 41 | # Set token (or environment variable VAULT_SYNC_SRC_TOKEN) 42 | # token: *** 43 | # token_ttl: 86400 # optional, 12h 44 | 45 | # Vault AppRole auth method 46 | # Set role_id and secret_id (or environment variables VAULT_SYNC_SRC_ROLE_ID and VAULT_SYNC_SRC_SECRET_ID) 47 | # role_id: *** 48 | # secret_id: *** 49 | # token_ttl: 86400 # optional, 12h 50 | # token_max_ttl: 2764800 # 32d 51 | 52 | # Destination Vault configuration to sync secrets to. 53 | dst: 54 | # Vault URL 55 | url: http://127.0.0.1:8200/ 56 | 57 | # Prefix for secrets: this prefix will replace the corresponding prefix from the 'src' section. 58 | # This allows syncing a tree of secrets to a non overlapping tree in the same Vault. 59 | # For example: if src.prefix is "src" and dst.prefix is "dst", then secret "src/secret1" will be 60 | # synced to "dst/secret1". 61 | prefix: "" 62 | 63 | # Vault namespace, not set by default. 64 | # namespace: null 65 | 66 | # Path for the secrets engine. If "backend" or "backends" not specified for here, then the 67 | # corresponding configuration for src will be used for dst. Note that currently only the following 68 | # cases are supported: 69 | # * one src backend to one dst backend 70 | # * multiple src backends to the same number of dst backends 71 | # Other cases (one to many, many to one, or different numbers of src and dst backends) are not 72 | # currently supported. 73 | # backend: secret 74 | # or 75 | # backends: 76 | # - secret1 77 | # - secret2 78 | 79 | # Secrets engine version, default is 2. 80 | # version: 2 81 | 82 | # Vault Token auth method 83 | # Set token (or environment variable VAULT_SYNC_DST_TOKEN) 84 | # token: *** 85 | # token_ttl: 86400 # optional, 12h 86 | 87 | # Vault AppRole auth method 88 | # Set role_id and secret_id (or environment variables VAULT_SYNC_DST_ROLE_ID and VAULT_SYNC_DST_SECRET_ID) 89 | # role_id: *** 90 | # secret_id: *** 91 | # token_ttl: 86400 # optional, 12h 92 | # token_max_ttl: 2764800 # 32d 93 | -------------------------------------------------------------------------------- /scripts/test-helm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Local test for vault-sync. Requires installed docker, kind, kubectl. 4 | 5 | set -e -o pipefail 6 | 7 | : ${GITHUB_SHA:=$(git describe --always)} 8 | 9 | function cleanup() { 10 | kind delete cluster || true 11 | } 12 | 13 | trap cleanup EXIT 14 | 15 | # Create Kubernetes cluster 16 | cat </dev/null | grep -q vault-0 &>/dev/null; then 33 | break 34 | fi 35 | sleep 1 36 | done 37 | kubectl --namespace=vault wait pod/vault-0 --for=condition=Ready --timeout=180s 38 | kubectl --namespace=vault logs vault-0 39 | 40 | # Create secret backends 41 | kubectl --namespace=vault exec vault-0 -- vault secrets enable -version=2 -path=src kv 42 | kubectl --namespace=vault exec vault-0 -- vault secrets enable -version=2 -path=dst kv 43 | kubectl --namespace=vault exec vault-0 -- vault kv put -mount src test1 foo=bar 44 | 45 | # Build Docker image 46 | docker build -t pbchekin/vault-sync:$GITHUB_SHA -f docker/Dockerfile . 47 | 48 | # Load Docker image to the cluster 49 | kind load docker-image pbchekin/vault-sync:$GITHUB_SHA 50 | 51 | # Deploy vault-sync 52 | kubectl create namespace vault-sync 53 | cd install/helm/vault-sync/ 54 | helm install --namespace=vault-sync vault-sync -f - . </dev/null | grep -q vault-sync &>/dev/null; then 75 | break 76 | fi 77 | sleep 1 78 | done 79 | if ! kubectl --namespace=vault-sync wait pod -l app.kubernetes.io/instance=vault-sync --for=condition=Ready --timeout=180s; then 80 | kubectl get pods -A 81 | kubectl --namespace=vault-sync logs -l app.kubernetes.io/instance=vault-sync 82 | exit 1 83 | fi 84 | 85 | # Check sync result 86 | sleep 5 87 | kubectl --namespace=vault exec vault-0 -- vault kv get -mount dst test1 88 | 89 | # Show vault-sync logs 90 | kubectl --namespace=vault-sync logs -l app.kubernetes.io/instance=vault-sync 91 | 92 | # Test external secret 93 | kubectl delete namespace vault-sync 94 | kubectl --namespace=vault exec vault-0 -- vault kv put -mount src test2 foo=bar 95 | kubectl create namespace vault-sync 96 | kubectl --namespace=vault-sync create secret generic vault-sync-secret \ 97 | --from-literal=VAULT_SYNC_SRC_TOKEN=root \ 98 | --from-literal=VAULT_SYNC_DST_TOKEN=root 99 | 100 | helm install --namespace=vault-sync vault-sync -f - . </dev/null; then 118 | break 119 | fi 120 | sleep 1 121 | done 122 | if ! kubectl --namespace=vault-sync wait pod -l app.kubernetes.io/instance=vault-sync --for=condition=Ready --timeout=180s; then 123 | kubectl get pods -A 124 | kubectl --namespace=vault-sync logs -l app.kubernetes.io/instance=vault-sync 125 | exit 1 126 | fi 127 | 128 | # Check sync result 129 | sleep 5 130 | kubectl --namespace=vault exec vault-0 -- vault kv get -mount dst test2 131 | 132 | # Show vault-sync logs 133 | kubectl --namespace=vault-sync logs -l app.kubernetes.io/instance=vault-sync 134 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{thread}; 2 | use std::error::Error; 3 | use std::net::TcpListener; 4 | use std::sync::{Arc, Mutex}; 5 | use std::sync::mpsc; 6 | use std::thread::JoinHandle; 7 | 8 | use clap::{crate_authors, crate_version, Arg, App}; 9 | use log::{error, info}; 10 | use simplelog::*; 11 | 12 | use config::{VaultHost, VaultSyncConfig}; 13 | use vault::VaultClient; 14 | use crate::config::{EngineVersion, get_backends}; 15 | 16 | mod audit; 17 | mod config; 18 | mod sync; 19 | mod vault; 20 | 21 | fn main() -> Result<(), Box> { 22 | TermLogger::init(LevelFilter::Info, Config::default(), TerminalMode::Mixed, ColorChoice::Auto)?; 23 | 24 | let matches = App::new("vault-sync") 25 | .author(crate_authors!()) 26 | .version(crate_version!()) 27 | .arg(Arg::with_name("config") 28 | .long("config") 29 | .value_name("FILE") 30 | .help("Configuration file") 31 | .default_value("./vault-sync.yaml") 32 | .takes_value(true)) 33 | .arg(Arg::with_name("dry-run") 34 | .long("dry-run") 35 | .help("Do not do any changes with the destination Vault")) 36 | .arg(Arg::with_name("once") 37 | .long("once") 38 | .help("Run the full sync once, then exit")) 39 | .get_matches(); 40 | 41 | let config = load_config(matches.value_of("config").unwrap())?; 42 | let (tx, rx): (mpsc::Sender, mpsc::Receiver) = mpsc::channel(); 43 | 44 | let log_sync = match &config.bind { 45 | Some(_) => Some(log_sync_worker(&config, tx.clone())?), 46 | None => None, 47 | }; 48 | info!("Connecting to {}", &config.src.host.url); 49 | let src_client = vault_client(&config.src.host, &config.src.version, config.src.namespace.clone())?; 50 | let shared_src_client = Arc::new(Mutex::new(src_client)); 51 | let src_token = token_worker(&config.src.host, &config.src.version, shared_src_client.clone(), config.src.namespace.clone()); 52 | 53 | info!("Connecting to {}", &config.dst.host.url); 54 | let dst_client = vault_client(&config.dst.host, &config.dst.version, config.dst.namespace.clone())?; 55 | let shared_dst_client = Arc::new(Mutex::new(dst_client)); 56 | let dst_token = token_worker(&config.dst.host, &config.dst.version, shared_dst_client.clone(), config.dst.namespace.clone()); 57 | 58 | info!( 59 | "Audit device {} exists: {}", 60 | &config.id, 61 | sync::audit_device_exists(&config.id, shared_src_client.clone()), 62 | ); 63 | 64 | let sync = sync_worker( 65 | rx, 66 | &config, 67 | shared_src_client.clone(), 68 | shared_dst_client.clone(), 69 | matches.is_present("dry-run"), 70 | matches.is_present("once"), 71 | ); 72 | 73 | let mut join_handlers = vec![sync]; 74 | 75 | if !matches.is_present("once") { 76 | let full_sync = full_sync_worker(&config, shared_src_client.clone(), tx.clone()); 77 | join_handlers.push(full_sync); 78 | join_handlers.push(src_token); 79 | join_handlers.push(dst_token); 80 | if log_sync.is_some() { 81 | join_handlers.push(log_sync.unwrap()); 82 | } 83 | } else { 84 | let backends = get_backends(&config.src.backend); 85 | sync::full_sync(&config.src.prefix, &backends, shared_src_client.clone(), tx.clone()); 86 | }; 87 | 88 | // Join all threads 89 | for handler in join_handlers { 90 | let _ = handler.join(); 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | fn load_config(file_name: &str) -> Result> { 97 | match VaultSyncConfig::from_file(file_name) { 98 | Ok(config) => { 99 | info!("Configuration from {}:\n{}", file_name, serde_json::to_string_pretty(&config).unwrap()); 100 | Ok(config) 101 | }, 102 | Err(error) => { 103 | error!("Failed to load configuration file {}: {}", file_name, error); 104 | Err(error) 105 | } 106 | } 107 | } 108 | 109 | fn vault_client(host: &VaultHost, version: &EngineVersion, namespace: Option) -> Result> { 110 | match vault::vault_client(host, version, namespace) { 111 | Ok(client) => { 112 | Ok(client) 113 | }, 114 | Err(error) => { 115 | error!("Failed to connect to {}: {}", &host.url, error); 116 | Err(error.into()) 117 | } 118 | } 119 | } 120 | 121 | fn token_worker(host: &VaultHost, version: &EngineVersion, client: Arc>, namespace: Option) -> JoinHandle<()> { 122 | let host = host.clone(); 123 | let version = version.clone(); 124 | thread::spawn(move || { 125 | vault::token_worker(&host, &version, client, namespace.clone()); 126 | }) 127 | } 128 | 129 | fn sync_worker( 130 | rx: mpsc::Receiver, 131 | config: &VaultSyncConfig, 132 | src_client: Arc>, 133 | dst_client: Arc>, 134 | dry_run: bool, 135 | run_once: bool, 136 | ) -> thread::JoinHandle<()> { 137 | info!("Dry run: {}", dry_run); 138 | let config = config.clone(); 139 | thread::spawn(move || { 140 | sync::sync_worker(rx, &config, src_client, dst_client, dry_run, run_once); 141 | }) 142 | } 143 | 144 | fn log_sync_worker(config: &VaultSyncConfig, tx: mpsc::Sender) -> Result, std::io::Error> { 145 | let addr = &config.bind.clone().unwrap(); 146 | let config = config.clone(); 147 | info!("Listening on {}", addr); 148 | let listener = TcpListener::bind(addr)?; 149 | let handle = thread::spawn(move || { 150 | for stream in listener.incoming() { 151 | if let Ok(stream) = stream { 152 | let tx = tx.clone(); 153 | let config = config.clone(); 154 | thread::spawn(move || { 155 | sync::log_sync(&config, stream, tx); 156 | }); 157 | } 158 | } 159 | }); 160 | Ok(handle) 161 | } 162 | 163 | fn full_sync_worker( 164 | config: &VaultSyncConfig, 165 | client: Arc>, 166 | tx: mpsc::Sender 167 | ) -> thread::JoinHandle<()>{ 168 | let config = config.clone(); 169 | thread::spawn(move || { 170 | sync::full_sync_worker(&config, client, tx); 171 | }) 172 | } 173 | -------------------------------------------------------------------------------- /src/vault.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | use std::sync::{Arc, Mutex}; 3 | use std::time::Duration; 4 | 5 | use hashicorp_vault::client as vault; 6 | use hashicorp_vault::client::{SecretsEngine, TokenData, VaultDuration}; 7 | use hashicorp_vault::client::error::Result as VaultResult; 8 | use log::{info, warn}; 9 | 10 | use crate::config::{EngineVersion, VaultAuthMethod, VaultHost}; 11 | 12 | pub type VaultClient = hashicorp_vault::client::VaultClient; 13 | 14 | pub fn vault_client(host: &VaultHost, version: &EngineVersion, namespace: Option) -> VaultResult> { 15 | let mut result = match host.auth.as_ref().unwrap() { 16 | VaultAuthMethod::TokenAuth { token } => { 17 | VaultClient::new(&host.url, token, namespace) 18 | }, 19 | VaultAuthMethod::AppRoleAuth { role_id, secret_id} => { 20 | let client = vault::VaultClient::new_app_role( 21 | &host.url, role_id, Some(secret_id), namespace.clone())?; 22 | VaultClient::new(&host.url, client.token, namespace) 23 | } 24 | }; 25 | 26 | if let Ok(client) = &mut result { 27 | client.secrets_engine( 28 | match version { 29 | EngineVersion::V1 => SecretsEngine::KVV1, 30 | EngineVersion::V2 => SecretsEngine::KVV2, 31 | } 32 | ); 33 | } 34 | 35 | result 36 | } 37 | 38 | // Worker to renew a Vault token lease, or to request a new token (for Vault AppRole auth method) 39 | pub fn token_worker(host: &VaultHost, version: &EngineVersion, client: Arc>, namespace: Option) { 40 | let mut token_age = time::Instant::now(); 41 | loop { 42 | let info = { 43 | let client = client.lock().unwrap(); 44 | TokenInfo::from_client(&client) 45 | }; 46 | info!("Token: {:?}", &info); 47 | 48 | // Override token TTL and max TTL with optional values from config 49 | let mut plan = info.clone(); 50 | if let Some(token_ttl) = &host.token_ttl { 51 | match &info.ttl { 52 | Some(ttl) => { 53 | if *token_ttl > 0 && *token_ttl < ttl.as_secs() { 54 | plan.ttl = Some(Duration::from_secs(*token_ttl)); 55 | } 56 | }, 57 | None => { 58 | plan.ttl = Some(Duration::from_secs(*token_ttl)) 59 | } 60 | } 61 | } 62 | if let Some(token_max_ttl) = &host.token_max_ttl { 63 | match &info.max_ttl { 64 | Some(max_ttl) => { 65 | if *token_max_ttl > 0 && *token_max_ttl < max_ttl.as_secs() { 66 | plan.max_ttl = Some(Duration::from_secs(*token_max_ttl)); 67 | } 68 | }, 69 | None => { 70 | plan.max_ttl = Some(Duration::from_secs(*token_max_ttl)) 71 | } 72 | } 73 | } 74 | info!("Plan: {:?}", &plan); 75 | 76 | if !plan.renewable { 77 | return; 78 | } else { 79 | if let Some(VaultAuthMethod::AppRoleAuth { role_id: _, secret_id: _ }) = &host.auth { 80 | if plan.max_ttl.is_none() { 81 | warn!("Auth method is AppRole, but max_ttl is not set, using 32 days instead"); 82 | plan.max_ttl = Some(time::Duration::from_secs(32 * 24 * 60 * 60)); 83 | } 84 | } 85 | if let Some(VaultAuthMethod::TokenAuth { token: _ }) = &host.auth { 86 | if plan.max_ttl.is_some() { 87 | info!("Auth method is Token, but max_ttl is set, ignoring"); 88 | plan.max_ttl = None; 89 | } 90 | } 91 | } 92 | 93 | let duration = { 94 | if plan.ttl.is_none() { 95 | plan.max_ttl.unwrap() 96 | } else if plan.max_ttl.is_none() { 97 | plan.ttl.unwrap() 98 | } else { 99 | plan.ttl.unwrap().min(plan.max_ttl.unwrap()) 100 | } 101 | }; 102 | let duration = time::Duration::from_secs(duration.as_secs() / 2); 103 | 104 | thread::sleep(duration); 105 | 106 | if let Some(max_ttl) = plan.max_ttl { 107 | let age = token_age.elapsed().as_secs(); 108 | let max_ttl = max_ttl.as_secs(); 109 | if age > max_ttl / 2 { 110 | if let Some(VaultAuthMethod::AppRoleAuth { role_id: _, secret_id: _ }) = &host.auth { 111 | info!("Requesting a new token"); 112 | match vault_client(&host, &version, namespace.clone()) { 113 | Ok(new_client) => { 114 | let mut client = client.lock().unwrap(); 115 | client.token = new_client.token; 116 | client.data = new_client.data; 117 | token_age = time::Instant::now(); 118 | continue; 119 | }, 120 | Err(error) => { 121 | warn!("Failed to request a new token: {}", error); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | if let Some(_) = plan.ttl { 129 | info!("Renewing token"); 130 | let result = { 131 | let mut client = client.lock().unwrap(); 132 | client.renew() 133 | }; 134 | if let Err(error) = result { 135 | warn!("Failed to renew token: {}", error); 136 | } 137 | } 138 | } 139 | } 140 | 141 | #[derive(Debug, Clone)] 142 | struct TokenInfo { 143 | renewable: bool, 144 | ttl: Option, 145 | max_ttl: Option, 146 | } 147 | 148 | impl TokenInfo { 149 | fn new() -> TokenInfo { 150 | // Defaults are for the root token, which is not renewable and has no TTL and max TTL 151 | TokenInfo { 152 | renewable: false, 153 | ttl: None, 154 | max_ttl: None, 155 | } 156 | } 157 | 158 | fn from_client(client: &VaultClient) -> TokenInfo { 159 | let mut info = Self::new(); 160 | if let Some(data) = &client.data { 161 | if let Some(data) = &data.data { 162 | let zero_duration = VaultDuration::seconds(0); 163 | info.renewable = data.renewable.unwrap_or(false); 164 | let ttl_duration= &data.ttl; 165 | if ttl_duration.0.as_secs() > 0 { 166 | info.ttl = Some(ttl_duration.0); 167 | } 168 | 169 | let max_ttl_duration = data.explicit_max_ttl.as_ref().unwrap_or(&zero_duration); 170 | if max_ttl_duration.0.as_secs() > 0 { 171 | info.max_ttl = Some(max_ttl_duration.0); 172 | } 173 | } 174 | } 175 | info 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault-sync 2 | 3 | A tool to replicate secrets from one HashiCorp Vault or OpenBao instance to another. 4 | 5 | ## How it works 6 | 7 | When vault-sync starts, it does a full copy of the secrets from the source Vault instance to the destination Vault instance. 8 | Periodically, vault-sync does a full reconciliation to make sure all the destination secrets are up to date. 9 | 10 | At the same time, you can manually enable the [Socket Audit Device](https://www.vaultproject.io/docs/audit/socket) for the source Vault, 11 | so Vault will be sending audit logs to vault-sync. 12 | Using these audit logs, vault-sync keeps the secrets in the destination Vault up to date. 13 | Note that vault-sync does not create or delete the audit devices by itself. 14 | 15 | It is possible to use the same Vault instance as the source and the destination. 16 | You can use this feature to replicate a "folder" of secrets to another "folder" on the same server. 17 | 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. 18 | 19 | ## Limitations 20 | 21 | * Only two Vault auth methods are supported: [Token](https://www.vaultproject.io/docs/auth/token) and [AppRole](https://www.vaultproject.io/docs/auth/approle) 22 | * Only secrets are replicated (specifically their latest versions) 23 | 24 | ## Configuration 25 | 26 | Use the [example](vault-sync.example.yaml) to create your own configuration file. 27 | Instead of specifying secrets in the configuration file, you can use environment variables: 28 | 29 | * For Token auth method: 30 | * `VAULT_SYNC_SRC_TOKEN` 31 | * `VAULT_SYNC_DST_TOKEN` 32 | * For AppRole auth method: 33 | * `VAULT_SYNC_SRC_ROLE_ID` 34 | * `VAULT_SYNC_SRC_SECRET_ID` 35 | * `VAULT_SYNC_DST_ROLE_ID` 36 | * `VAULT_SYNC_DST_SECRET_ID` 37 | 38 | ### Source Vault 39 | 40 | A token or AppRole for the source Vault should have a policy that allows listing and reading secrets: 41 | 42 | For [KV secrets engine v1](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1): 43 | 44 | ```shell 45 | cat <, 37 | pub token_ttl: Option, 38 | pub token_max_ttl: Option, 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Clone, Debug)] 42 | pub enum Backend { 43 | #[serde(rename = "backend")] 44 | Backend(String), 45 | #[serde(rename = "backends")] 46 | Backends(Vec), 47 | } 48 | 49 | #[derive(Serialize, Deserialize, Clone, Debug)] 50 | pub struct VaultSource { 51 | #[serde(flatten)] 52 | pub host: VaultHost, 53 | #[serde(default)] 54 | pub prefix: String, 55 | #[serde(flatten)] 56 | pub backend: Option, 57 | #[serde(default)] 58 | pub version: EngineVersion, 59 | pub namespace: Option, 60 | } 61 | 62 | #[derive(Serialize, Deserialize, Clone, Debug)] 63 | pub struct VaultDestination { 64 | #[serde(flatten)] 65 | pub host: VaultHost, 66 | #[serde(default)] 67 | pub prefix: String, 68 | #[serde(flatten)] 69 | pub backend: Option, 70 | #[serde(default)] 71 | pub version: EngineVersion, 72 | pub namespace: Option, 73 | } 74 | 75 | #[derive(Serialize, Deserialize, Clone, Debug)] 76 | pub struct VaultSyncConfig { 77 | pub id: String, 78 | pub full_sync_interval: u64, 79 | pub bind: Option, 80 | pub src: VaultSource, 81 | pub dst: VaultDestination, 82 | } 83 | 84 | #[derive(Debug, Clone)] 85 | pub enum ConfigError { 86 | AuthRequired, 87 | OneToManyNotSupported, 88 | ManyToOneNotSupported, 89 | DifferentNumberOfBackends, 90 | } 91 | 92 | // Returns backend or backends as a vector. 93 | pub fn get_backends(backend: &Option) -> Vec { 94 | match backend { 95 | Some(Backend::Backend(backend)) => vec![backend.into()], 96 | Some(Backend::Backends(backends)) => backends.clone(), 97 | _ => panic!("Not implemented"), 98 | } 99 | } 100 | 101 | impl Default for EngineVersion { 102 | fn default() -> Self { 103 | EngineVersion::V2 104 | } 105 | } 106 | 107 | impl VaultSyncConfig { 108 | pub fn from_file(file_name: &str) -> Result> { 109 | let file = File::open(file_name)?; 110 | let mut config: VaultSyncConfig = serde_yaml::from_reader(file)?; 111 | config.auth_from_env()?; 112 | config.defaults()?; 113 | config.validate()?; 114 | Ok(config) 115 | } 116 | 117 | fn auth_from_env(&mut self) -> Result<(), Box> { 118 | if self.src.host.auth.is_none() { 119 | self.src.host.auth = Some(VaultAuthMethod::from_env("VAULT_SYNC_SRC")?); 120 | } 121 | if self.dst.host.auth.is_none() { 122 | self.dst.host.auth = Some(VaultAuthMethod::from_env("VAULT_SYNC_DST")?); 123 | } 124 | Ok(()) 125 | } 126 | 127 | fn defaults(&mut self) -> Result<(), Box> { 128 | if self.src.backend.is_none() { 129 | self.src.backend = Some(Backend::Backend("secret".into())); 130 | } 131 | if self.dst.backend.is_none() { 132 | self.dst.backend = self.src.backend.clone(); 133 | } 134 | Ok(()) 135 | } 136 | 137 | fn validate(&self) -> Result<(), Box> { 138 | let src_backend = self.src.backend.as_ref().unwrap(); 139 | let dst_backend = self.dst.backend.as_ref().unwrap(); 140 | 141 | match &src_backend { 142 | Backend::Backend(_) => match &dst_backend { 143 | Backend::Backends(_) => { 144 | return Err(ConfigError::OneToManyNotSupported.into()); 145 | }, 146 | _ => {}, 147 | }, 148 | Backend::Backends(src_backends) => match &dst_backend { 149 | Backend::Backend(_) => { 150 | return Err(ConfigError::ManyToOneNotSupported.into()); 151 | }, 152 | Backend::Backends(dst_backends) => { 153 | if src_backends.len() != dst_backends.len() { 154 | return Err(ConfigError::DifferentNumberOfBackends.into()); 155 | } 156 | } 157 | } 158 | } 159 | Ok(()) 160 | } 161 | } 162 | 163 | impl VaultAuthMethod { 164 | fn from_env(prefix: &str) -> Result> { 165 | let token = env::var(format!("{}_TOKEN", prefix)); 166 | let role_id = env::var(format!("{}_ROLE_ID", prefix)); 167 | let secret_id = env::var(format!("{}_SECRET_ID", prefix)); 168 | if let Ok(token) = token { 169 | return Ok(VaultAuthMethod::TokenAuth { token }) 170 | } 171 | if let (Ok(role_id), Ok(secret_id)) = (role_id, secret_id) { 172 | return Ok(VaultAuthMethod::AppRoleAuth { role_id, secret_id }) 173 | } 174 | Err(ConfigError::AuthRequired.into()) 175 | } 176 | } 177 | 178 | impl fmt::Display for ConfigError { 179 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 180 | match *self { 181 | ConfigError::AuthRequired => 182 | write!(f, "Vault token or both app role id and secret id are required"), 183 | ConfigError::OneToManyNotSupported => 184 | write!(f, "Syncing one backend to many not supported"), 185 | ConfigError::ManyToOneNotSupported => 186 | write!(f, "Syncing many backends to one not supported"), 187 | ConfigError::DifferentNumberOfBackends => 188 | write!(f, "Different number of backends for source and destination"), 189 | } 190 | } 191 | } 192 | 193 | impl Error for ConfigError { 194 | } 195 | 196 | fn sanitize(_: &str, s: S) -> Result 197 | where 198 | S: Serializer, 199 | { 200 | s.serialize_str("***") 201 | } 202 | 203 | #[cfg(test)] 204 | mod tests { 205 | use std::error::Error; 206 | use crate::config::{EngineVersion, VaultSyncConfig, get_backends, ConfigError}; 207 | 208 | #[test] 209 | fn test_load() -> Result<(), Box> { 210 | let yaml = r#" 211 | id: vault-sync-id 212 | full_sync_interval: 60 213 | bind: 0.0.0.0:8202 214 | src: 215 | url: http://127.0.0.1:8200/ 216 | prefix: src 217 | dst: 218 | url: http://127.0.0.1:8200/ 219 | prefix: dst 220 | version: 1 221 | "#; 222 | let mut config: VaultSyncConfig = serde_yaml::from_str(yaml)?; 223 | config.defaults()?; 224 | assert_eq!(config.id, "vault-sync-id"); 225 | assert_eq!(config.bind, Some("0.0.0.0:8202".to_string())); 226 | assert_eq!(config.src.version, EngineVersion::V2); 227 | assert_eq!(config.dst.version, EngineVersion::V1); 228 | Ok(()) 229 | } 230 | 231 | fn render_yaml( 232 | src: Option<&str>, 233 | dst: Option<&str>, 234 | src_key: &str, 235 | dst_key: &str, 236 | ) -> String { 237 | format!( 238 | r#" 239 | id: vault-sync-id 240 | full_sync_interval: 60 241 | src: 242 | url: http://127.0.0.1:8200/ 243 | {} 244 | dst: 245 | url: http://127.0.0.1:8200/ 246 | {} 247 | "#, 248 | src.map_or("".to_string(), |v| format!("{}: {}", src_key, v)), 249 | dst.map_or("".to_string(), |v| format!("{}: {}", dst_key, v)), 250 | ) 251 | } 252 | 253 | fn render_backend_yaml(src: Option<&str>, dst: Option<&str>) -> String { 254 | render_yaml(src, dst, "backend", "backend") 255 | } 256 | 257 | fn render_backends_yaml(src: Option<&str>, dst: Option<&str>) -> String { 258 | render_yaml(src, dst, "backends", "backends") 259 | } 260 | 261 | fn test_single_backend( 262 | src: Option<&str>, 263 | dst: Option<&str>, 264 | expected_src: &str, 265 | expected_dst: &str, 266 | ) -> Result<(), Box> { 267 | let yaml = render_backend_yaml(src, dst); 268 | let mut config: VaultSyncConfig = serde_yaml::from_str(&yaml)?; 269 | config.defaults()?; 270 | config.validate()?; 271 | assert_eq!(get_backends(&config.src.backend).first().unwrap(), expected_src); 272 | assert_eq!(get_backends(&config.dst.backend).first().unwrap(), expected_dst); 273 | Ok(()) 274 | } 275 | 276 | fn test_many_backends( 277 | src: Option<&str>, 278 | dst: Option<&str>, 279 | expected_src: &[&str], 280 | expected_dst: &[&str], 281 | ) -> Result<(), Box> { 282 | let yaml = render_backends_yaml(src, dst); 283 | let mut config: VaultSyncConfig = serde_yaml::from_str(&yaml)?; 284 | config.defaults()?; 285 | config.validate()?; 286 | assert_eq!(get_backends(&config.src.backend), expected_src); 287 | assert_eq!(get_backends(&config.dst.backend), expected_dst); 288 | Ok(()) 289 | } 290 | 291 | #[test] 292 | fn test_backends() -> Result<(), Box> { 293 | test_single_backend(None, None, "secret", "secret")?; 294 | test_single_backend(Some("custom"), None, "custom", "custom")?; 295 | test_single_backend(None, Some("custom"), "secret", "custom")?; 296 | test_single_backend(Some("src"), Some("dst"), "src", "dst")?; 297 | test_many_backends(Some("[foo, baz]"), None, &["foo", "baz"], &["foo", "baz"])?; 298 | test_many_backends(Some("[foo, baz]"), Some("[bar, qux]"), &["foo", "baz"], &["bar", "qux"])?; 299 | Ok(()) 300 | } 301 | 302 | #[test] 303 | fn test_one_to_many_error() -> Result<(), Box> { 304 | let yaml = render_yaml(Some(""), Some("[foo, baz]"), "backend", "backends"); 305 | let mut config: VaultSyncConfig = serde_yaml::from_str(&yaml)?; 306 | config.defaults()?; 307 | let result = config.validate(); 308 | assert!(result.is_err()); 309 | assert_eq!(result.unwrap_err().to_string(), ConfigError::OneToManyNotSupported.to_string()); 310 | Ok(()) 311 | } 312 | 313 | #[test] 314 | fn test_many_to_one_error() -> Result<(), Box> { 315 | let yaml = render_yaml(Some("[foo, baz]"), Some("bar"), "backends", "backend"); 316 | let mut config: VaultSyncConfig = serde_yaml::from_str(&yaml)?; 317 | config.defaults()?; 318 | let result = config.validate(); 319 | assert!(result.is_err()); 320 | assert_eq!(result.unwrap_err().to_string(), ConfigError::ManyToOneNotSupported.to_string()); 321 | Ok(()) 322 | } 323 | 324 | #[test] 325 | fn test_different_numbers_of_backend() -> Result<(), Box> { 326 | let yaml = render_yaml(Some("[foo]"), Some("[baz, bar]"), "backends", "backends"); 327 | let mut config: VaultSyncConfig = serde_yaml::from_str(&yaml)?; 328 | config.defaults()?; 329 | let result = config.validate(); 330 | assert!(result.is_err()); 331 | assert_eq!(result.unwrap_err().to_string(), ConfigError::DifferentNumberOfBackends.to_string()); 332 | Ok(()) 333 | } 334 | } -------------------------------------------------------------------------------- /vault-rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_docs, 3 | missing_debug_implementations, 4 | trivial_casts, 5 | trivial_numeric_casts, 6 | unsafe_code, 7 | unstable_features, 8 | unused_import_braces, 9 | unused_qualifications, 10 | unused_results 11 | )] 12 | #![cfg_attr(test, deny(warnings))] 13 | #![cfg_attr(feature = "clippy", allow(unstable_features))] 14 | #![cfg_attr(feature = "clippy", feature(plugin))] 15 | #![cfg_attr(feature = "clippy", plugin(clippy))] 16 | #![cfg_attr(feature = "clippy", deny(clippy))] 17 | 18 | //! Client API for interacting with [Vault](https://www.vaultproject.io/docs/http/index.html) 19 | 20 | extern crate base64; 21 | extern crate reqwest; 22 | #[macro_use] 23 | extern crate log; 24 | #[macro_use] 25 | extern crate quick_error; 26 | pub extern crate chrono; 27 | extern crate serde; 28 | pub extern crate url; 29 | 30 | /// vault client 31 | pub mod client; 32 | pub use crate::client::error::{Error, Result}; 33 | pub use crate::client::VaultClient as Client; 34 | use url::Url; 35 | 36 | /// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417 37 | /// 38 | /// An attempted conversion that consumes `self`, which may or may not be expensive. 39 | /// 40 | /// Library authors should not directly implement this trait, but should prefer implementing 41 | /// the [`TryFrom`] trait, which offers greater flexibility and provides an equivalent `TryInto` 42 | /// implementation for free, thanks to a blanket implementation in the standard library. 43 | /// 44 | /// [`TryFrom`]: trait.TryFrom.html 45 | pub trait TryInto: Sized { 46 | /// The type returned in the event of a conversion error. 47 | type Err; 48 | 49 | /// Performs the conversion. 50 | fn try_into(self) -> ::std::result::Result; 51 | } 52 | 53 | /// Waiting to stabilize: https://github.com/rust-lang/rust/issues/33417 54 | /// 55 | /// Attempt to construct `Self` via a conversion. 56 | pub trait TryFrom: Sized { 57 | /// The type returned in the event of a conversion error. 58 | type Err; 59 | 60 | /// Performs the conversion. 61 | fn try_from(_: T) -> ::std::result::Result; 62 | } 63 | 64 | impl TryInto for T 65 | where 66 | U: TryFrom, 67 | { 68 | type Err = U::Err; 69 | 70 | fn try_into(self) -> ::std::result::Result { 71 | U::try_from(self) 72 | } 73 | } 74 | 75 | impl TryFrom for Url { 76 | type Err = Error; 77 | fn try_from(u: Url) -> ::std::result::Result { 78 | Ok(u) 79 | } 80 | } 81 | 82 | impl<'a> TryFrom<&'a Url> for Url { 83 | type Err = Error; 84 | fn try_from(u: &Url) -> ::std::result::Result { 85 | Ok(u.clone()) 86 | } 87 | } 88 | 89 | impl<'a> TryFrom<&'a str> for Url { 90 | type Err = Error; 91 | fn try_from(s: &str) -> ::std::result::Result { 92 | match Url::parse(s) { 93 | Ok(u) => Ok(u), 94 | Err(e) => Err(e.into()), 95 | } 96 | } 97 | } 98 | 99 | impl<'a> TryFrom<&'a String> for Url { 100 | type Err = Error; 101 | fn try_from(s: &String) -> ::std::result::Result { 102 | match Url::parse(s) { 103 | Ok(u) => Ok(u), 104 | Err(e) => Err(e.into()), 105 | } 106 | } 107 | } 108 | 109 | impl TryFrom for Url { 110 | type Err = Error; 111 | fn try_from(s: String) -> ::std::result::Result { 112 | match Url::parse(&s) { 113 | Ok(u) => Ok(u), 114 | Err(e) => Err(e.into()), 115 | } 116 | } 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use crate::client::HttpVerb::*; 122 | use crate::client::VaultClient as Client; 123 | use crate::client::{self, EndpointResponse}; 124 | use crate::Error; 125 | use reqwest::StatusCode; 126 | use serde::{Deserialize, Serialize}; 127 | use serde_json::Value; 128 | 129 | /// vault host for testing 130 | const HOST: &str = "http://127.0.0.1:8200"; 131 | /// root token needed for testing 132 | const TOKEN: &str = "test12345"; 133 | 134 | #[test] 135 | fn it_can_create_a_client() { 136 | let _ = Client::new(HOST, TOKEN).unwrap(); 137 | } 138 | 139 | #[test] 140 | fn it_can_create_a_client_from_a_string_reference() { 141 | let _ = Client::new(&HOST.to_string(), TOKEN).unwrap(); 142 | } 143 | 144 | #[test] 145 | fn it_can_create_a_client_from_a_string() { 146 | let _ = Client::new(HOST.to_string(), TOKEN).unwrap(); 147 | } 148 | 149 | #[test] 150 | fn it_can_query_secrets() { 151 | let client = Client::new(HOST, TOKEN).unwrap(); 152 | let res = client.set_secret("hello_query", "world"); 153 | assert!(res.is_ok()); 154 | let res = client.get_secret("hello_query").unwrap(); 155 | assert_eq!(res, "world"); 156 | } 157 | 158 | #[test] 159 | fn it_can_store_json_secrets() { 160 | let client = Client::new(HOST, TOKEN).unwrap(); 161 | let json = "{\"foo\": {\"bar\": [\"baz\"]}}"; 162 | let res = client.set_secret("json_secret", json); 163 | assert!(res.is_ok()); 164 | let res = client.get_secret("json_secret").unwrap(); 165 | assert_eq!(res, json) 166 | } 167 | 168 | #[test] 169 | fn it_can_list_secrets() { 170 | let client = Client::new(HOST, TOKEN).unwrap(); 171 | 172 | let _res = client.set_secret("hello/fred", "world").unwrap(); 173 | // assert!(res.is_ok()); 174 | let res = client.set_secret("hello/bob", "world"); 175 | assert!(res.is_ok()); 176 | 177 | let res = client.list_secrets("hello"); 178 | assert!(res.is_ok()); 179 | assert_eq!(res.unwrap(), ["bob", "fred"]); 180 | 181 | let res = client.list_secrets("hello/"); 182 | assert!(res.is_ok()); 183 | assert_eq!(res.unwrap(), ["bob", "fred"]); 184 | } 185 | 186 | #[test] 187 | fn it_can_detect_404_status() { 188 | let client = Client::new(HOST, TOKEN).unwrap(); 189 | 190 | let res = client.list_secrets("non/existent/key"); 191 | assert!(res.is_err()); 192 | 193 | if let Err(Error::VaultResponse(_, response)) = res { 194 | assert_eq!(response.status(), StatusCode::NOT_FOUND); 195 | } else { 196 | panic!("Error should match on VaultResponse with reqwest response."); 197 | } 198 | } 199 | 200 | #[test] 201 | fn it_can_write_secrets_with_newline() { 202 | let client = Client::new(HOST, TOKEN).unwrap(); 203 | 204 | let res = client.set_secret("hello_set", "world\n"); 205 | assert!(res.is_ok()); 206 | let res = client.get_secret("hello_set").unwrap(); 207 | assert_eq!(res, "world\n"); 208 | } 209 | 210 | #[test] 211 | fn it_returns_err_on_forbidden() { 212 | let client = Client::new(HOST, "test123456"); 213 | // assert_eq!(Err("Forbidden".to_string()), client); 214 | assert!(client.is_err()); 215 | } 216 | 217 | #[test] 218 | fn it_can_delete_a_secret() { 219 | let client = Client::new(HOST, TOKEN).unwrap(); 220 | 221 | let res = client.set_secret("hello_delete", "world"); 222 | assert!(res.is_ok()); 223 | let res = client.get_secret("hello_delete").unwrap(); 224 | assert_eq!(res, "world"); 225 | let res = client.delete_secret("hello_delete"); 226 | assert!(res.is_ok()); 227 | let res = client.get_secret("hello_delete"); 228 | assert!(res.is_err()); 229 | } 230 | 231 | #[test] 232 | fn it_can_perform_approle_workflow() { 233 | use std::collections::HashMap; 234 | 235 | let c = Client::new(HOST, TOKEN).unwrap(); 236 | let mut body = "{\"type\":\"approle\"}"; 237 | // Ensure we do not currently have an approle backend enabled. 238 | // Older vault versions (<1.2.0) seem to have an AppRole backend 239 | // enabled by default, so calling the POST to create a new one 240 | // fails with a 400 status 241 | let _: EndpointResponse<()> = c 242 | .call_endpoint(DELETE, "sys/auth/approle", None, None) 243 | .unwrap(); 244 | // enable approle auth backend 245 | let mut res: EndpointResponse<()> = c 246 | .call_endpoint(POST, "sys/auth/approle", None, Some(body)) 247 | .unwrap(); 248 | panic_non_empty(&res); 249 | // make a new approle 250 | body = "{\"secret_id_ttl\":\"10m\", \"token_ttl\":\"20m\", \"token_max_ttl\":\"30m\", \ 251 | \"secret_id_num_uses\":40}"; 252 | res = c 253 | .call_endpoint(POST, "auth/approle/role/test_role", None, Some(body)) 254 | .unwrap(); 255 | panic_non_empty(&res); 256 | 257 | // let's test the properties endpoint while we're here 258 | let _ = c.get_app_role_properties("test_role").unwrap(); 259 | 260 | // get approle's role-id 261 | let res: EndpointResponse> = c 262 | .call_endpoint(GET, "auth/approle/role/test_role/role-id", None, None) 263 | .unwrap(); 264 | let data = match res { 265 | EndpointResponse::VaultResponse(res) => res.data.unwrap(), 266 | _ => panic!("expected vault response, got: {:?}", res), 267 | }; 268 | let role_id = &data["role_id"]; 269 | assert!(!role_id.is_empty()); 270 | 271 | // now get a secret id for this approle 272 | let res: EndpointResponse> = c 273 | .call_endpoint(POST, "auth/approle/role/test_role/secret-id", None, None) 274 | .unwrap(); 275 | let data = match res { 276 | EndpointResponse::VaultResponse(res) => res.data.unwrap(), 277 | _ => panic!("expected vault response, got: {:?}", res), 278 | }; 279 | let secret_id = &data["secret_id"].as_str().unwrap(); 280 | 281 | // now finally we can try to actually login! 282 | let _ = Client::new_app_role(HOST, &role_id[..], Some(&secret_id[..])).unwrap(); 283 | 284 | // clean up by disabling approle auth backend 285 | let res = c 286 | .call_endpoint(DELETE, "sys/auth/approle", None, None) 287 | .unwrap(); 288 | panic_non_empty(&res); 289 | } 290 | 291 | #[test] 292 | fn it_can_read_a_wrapped_secret() { 293 | let client = Client::new(HOST, TOKEN).unwrap(); 294 | let res = client.set_secret("hello_delete_2", "second world"); 295 | assert!(res.is_ok()); 296 | // wrap the secret's value in `sys/wrapping/unwrap` with a TTL of 2 minutes 297 | let res = client.get_secret_wrapped("hello_delete_2", "2m").unwrap(); 298 | let wrapping_token = res.wrap_info.unwrap().token; 299 | // make a new client with the wrapping token 300 | let c2 = Client::new_no_lookup(HOST, wrapping_token).unwrap(); 301 | // read the cubbyhole response (can only do this once!) 302 | let res = c2.get_unwrapped_response().unwrap(); 303 | assert_eq!(res.data.unwrap()["value"], "second world"); 304 | } 305 | 306 | #[test] 307 | fn it_can_store_policies() { 308 | // use trailing slash for host to ensure Url processing fixes this later 309 | let c = Client::new("http://127.0.0.1:8200/", TOKEN).unwrap(); 310 | let body = "{\"policy\":\"{}\"}"; 311 | // enable approle auth backend 312 | let res: EndpointResponse<()> = c 313 | .call_endpoint(PUT, "sys/policy/test_policy_1", None, Some(body)) 314 | .unwrap(); 315 | panic_non_empty(&res); 316 | let res: EndpointResponse<()> = c 317 | .call_endpoint(PUT, "sys/policy/test_policy_2", None, Some(body)) 318 | .unwrap(); 319 | panic_non_empty(&res); 320 | let client_policies = c.policies().unwrap(); 321 | let expected_policies = ["default", "test_policy_1", "test_policy_2", "root"]; 322 | let _ = expected_policies 323 | .iter() 324 | .map(|p| { 325 | assert!(client_policies.contains(&(*p).to_string())); 326 | }) 327 | .last(); 328 | let token_name = "policy_test_token".to_string(); 329 | let token_opts = client::TokenOptions::default() 330 | .policies(vec!["test_policy_1", "test_policy_2"].into_iter()) 331 | .default_policy(false) 332 | .id(&token_name[..]) 333 | .ttl(client::VaultDuration::minutes(1)); 334 | let _ = c.create_token(&token_opts).unwrap(); 335 | let body = format!("{{\"token\":\"{}\"}}", &token_name); 336 | let res: EndpointResponse = c 337 | .call_endpoint(POST, "auth/token/lookup", None, Some(&body)) 338 | .unwrap(); 339 | match res { 340 | EndpointResponse::VaultResponse(res) => { 341 | let data = res.data.unwrap(); 342 | let mut policies = data.policies; 343 | policies.sort(); 344 | assert_eq!(policies, ["test_policy_1", "test_policy_2"]); 345 | } 346 | _ => panic!("expected vault response, got: {:?}", res), 347 | } 348 | // clean-up 349 | let res: EndpointResponse<()> = c 350 | .call_endpoint(DELETE, "sys/policy/test_policy_1", None, None) 351 | .unwrap(); 352 | panic_non_empty(&res); 353 | let res: EndpointResponse<()> = c 354 | .call_endpoint(DELETE, "sys/policy/test_policy_2", None, None) 355 | .unwrap(); 356 | panic_non_empty(&res); 357 | } 358 | 359 | #[test] 360 | fn it_can_list_things() { 361 | let c = Client::new(HOST, TOKEN).unwrap(); 362 | let _ = c 363 | .create_token(&client::TokenOptions::default().ttl(client::VaultDuration::minutes(1))) 364 | .unwrap(); 365 | let res: EndpointResponse = c 366 | .call_endpoint(LIST, "auth/token/accessors", None, None) 367 | .unwrap(); 368 | match res { 369 | EndpointResponse::VaultResponse(res) => { 370 | let data = res.data.unwrap(); 371 | assert!(data.keys.len() > 2); 372 | } 373 | _ => panic!("expected vault response, got: {:?}", res), 374 | } 375 | } 376 | 377 | #[test] 378 | fn it_can_encrypt_decrypt_transit() { 379 | let key_id = "test-vault-rs"; 380 | let plaintext = b"data\0to\0encrypt"; 381 | 382 | let client = Client::new(HOST, TOKEN).unwrap(); 383 | let enc_resp = client.transit_encrypt(None, key_id, plaintext); 384 | let encrypted = enc_resp.unwrap(); 385 | let dec_resp = client.transit_decrypt(None, key_id, encrypted); 386 | let payload = dec_resp.unwrap(); 387 | assert_eq!(plaintext, payload.as_slice()); 388 | } 389 | 390 | // helper fn to panic on empty responses 391 | fn panic_non_empty(res: &EndpointResponse<()>) { 392 | match *res { 393 | EndpointResponse::Empty => {} 394 | _ => panic!("expected empty response, received: {:?}", res), 395 | } 396 | } 397 | 398 | #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] 399 | struct CustomSecretType { 400 | name: String, 401 | } 402 | 403 | #[test] 404 | fn it_can_set_and_get_a_custom_secret_type() { 405 | let input = CustomSecretType { 406 | name: "test".into(), 407 | }; 408 | 409 | let client = Client::new(HOST, TOKEN).unwrap(); 410 | 411 | let res = client.set_custom_secret("custom_type", &input); 412 | assert!(res.is_ok()); 413 | let res: CustomSecretType = client.get_custom_secret("custom_type").unwrap(); 414 | assert_eq!(res, input); 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/sync.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time}; 2 | use std::collections::HashMap; 3 | use std::io::{BufRead, BufReader}; 4 | use std::net::TcpStream; 5 | use std::sync::{Arc, Mutex}; 6 | use std::sync::mpsc; 7 | 8 | use hashicorp_vault::client::{EndpointResponse, HttpVerb}; 9 | use log::{debug, info, warn}; 10 | use serde_json::Value; 11 | 12 | use crate::audit; 13 | use crate::config::{EngineVersion, get_backends, VaultSyncConfig}; 14 | use crate::vault::VaultClient; 15 | 16 | pub fn audit_device_exists(name: &str, client: Arc>) -> bool { 17 | let client = client.lock().unwrap(); 18 | let name = format!("{}/", name); 19 | match client.call_endpoint::(HttpVerb::GET, "sys/audit", None, None) { 20 | Ok(response) => { 21 | debug!("GET sys/audit: {:?}", response); 22 | if let EndpointResponse::VaultResponse(response) = response { 23 | if let Some(Value::Object(map)) = response.data { 24 | for (key, _) in &map { 25 | if key == &name { 26 | return true; 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | Err(error) => { 33 | warn!("GET sys/audit: {}", error); 34 | } 35 | } 36 | false 37 | } 38 | 39 | pub fn full_sync_worker( 40 | config: &VaultSyncConfig, 41 | client: Arc>, 42 | tx: mpsc::Sender 43 | ) { 44 | info!("FullSync worker started"); 45 | let interval = time::Duration::from_secs(config.full_sync_interval); 46 | let prefix = &config.src.prefix; 47 | let backends = get_backends(&config.src.backend); 48 | loop { 49 | full_sync(prefix, &backends, client.clone(), tx.clone()); 50 | thread::sleep(interval); 51 | } 52 | } 53 | 54 | struct Item { 55 | parent: String, 56 | secrets: Option>, 57 | index: usize, 58 | } 59 | 60 | pub fn full_sync(prefix: &str, backends: &Vec, client: Arc>, tx: mpsc::Sender) { 61 | let prefix= normalize_prefix(prefix); 62 | info!("FullSync started"); 63 | let now = time::Instant::now(); 64 | for backend in backends { 65 | full_sync_internal(&prefix, backend, client.clone(), tx.clone()); 66 | } 67 | info!("FullSync finished in {}ms", now.elapsed().as_millis()); 68 | } 69 | 70 | fn full_sync_internal(prefix: &str, backend: &str, client: Arc>, tx: mpsc::Sender) { 71 | let mut stack: Vec = Vec::new(); 72 | let item = Item { 73 | parent: prefix.to_string(), 74 | secrets: None, 75 | index: 0, 76 | }; 77 | stack.push(item); 78 | 79 | 'outer: while stack.len() > 0 { 80 | let len = stack.len(); 81 | let item = stack.get_mut(len - 1).unwrap(); 82 | if item.secrets.is_none() { 83 | let secrets = { 84 | let mut client = client.lock().unwrap(); 85 | client.secret_backend(backend); 86 | client.list_secrets(&item.parent) 87 | }; 88 | match secrets { 89 | Ok(secrets) => { 90 | item.secrets = Some(secrets); 91 | }, 92 | Err(error) => { 93 | warn!("Failed to list secrets in {}: {}", &item.parent, error); 94 | } 95 | } 96 | } 97 | if let Some(secrets) = &item.secrets { 98 | while item.index < secrets.len() { 99 | let secret = &secrets[item.index]; 100 | item.index += 1; 101 | if secret.ends_with("/") { 102 | let item = Item { 103 | parent: format!("{}{}", &item.parent, secret), 104 | secrets: None, 105 | index: 0, 106 | }; 107 | stack.push(item); 108 | continue 'outer; 109 | } else { 110 | let full_name = format!("{}{}", &item.parent, &secret); 111 | let op = SecretOp::Create(SecretPath { mount: backend.to_string(), path: full_name }); 112 | if let Err(error) = tx.send(op) { 113 | warn!("Failed to send a secret to a sync thread: {}", error); 114 | } 115 | } 116 | } 117 | } 118 | stack.pop(); 119 | } 120 | let _ = tx.send(SecretOp::FullSyncFinished); 121 | } 122 | 123 | pub fn log_sync(config: &VaultSyncConfig, stream: TcpStream, tx: mpsc::Sender) { 124 | match stream.peer_addr() { 125 | Ok(peer_addr) => { 126 | info!("New connection from {}", peer_addr); 127 | }, 128 | Err(_) => { 129 | info!("New connection"); 130 | } 131 | } 132 | let backends = get_backends(&config.src.backend); 133 | let prefix = &config.src.prefix; 134 | let version = &config.src.version; 135 | 136 | let mut reader = BufReader::new(stream); 137 | loop { 138 | let mut line = String::new(); 139 | match reader.read_line(&mut line) { 140 | Ok(0) => { 141 | // EOF 142 | break; 143 | }, 144 | Ok(_) => { 145 | debug!("Log: '{}'", line.trim()); 146 | let audit_log: Result = serde_json::from_str(&line); 147 | match audit_log { 148 | Ok(audit_log) => { 149 | if let Some(op) = audit_log_op(&backends, &prefix, &version, &audit_log) { 150 | if let Err(error) = tx.send(op) { 151 | warn!("Failed to send a secret to a sync thread: {}", error); 152 | } 153 | } 154 | }, 155 | Err(error) => { 156 | warn!("Failed to deserialize: {}, response: {}", error, &line); 157 | } 158 | } 159 | }, 160 | Err(error) => { 161 | warn!("Error: {}", error); 162 | break; 163 | } 164 | } 165 | } 166 | debug!("Closed connection"); 167 | } 168 | 169 | #[derive(Debug)] 170 | pub struct SecretPath{ 171 | mount: String, 172 | path: String, 173 | } 174 | 175 | #[derive(Debug)] 176 | pub enum SecretOp { 177 | Create(SecretPath), 178 | Update(SecretPath), 179 | Delete(SecretPath), 180 | FullSyncFinished, 181 | } 182 | 183 | struct SyncStats { 184 | updated: u64, 185 | deleted: u64, 186 | } 187 | 188 | impl SyncStats { 189 | fn new() -> SyncStats { 190 | SyncStats { updated: 0, deleted: 0 } 191 | } 192 | fn reset(&mut self) { 193 | self.updated = 0; 194 | self.deleted = 0; 195 | } 196 | } 197 | 198 | pub fn sync_worker( 199 | rx: mpsc::Receiver, 200 | config: &VaultSyncConfig, 201 | src_client: Arc>, 202 | dst_client: Arc>, 203 | dry_run: bool, 204 | run_once: bool, 205 | ) { 206 | let src_prefix = normalize_prefix(&config.src.prefix); 207 | let dst_prefix = normalize_prefix(&config.dst.prefix); 208 | let src_mounts = get_backends(&config.src.backend); 209 | let dst_mounts = get_backends(&config.dst.backend); 210 | let mount_map: HashMap<&str, &str> = src_mounts.iter().map(|s| s.as_str()).zip(dst_mounts.iter().map(|s| s.as_str())).collect(); 211 | info!("Sync worker started"); 212 | let mut stats = SyncStats::new(); 213 | loop { 214 | let op = rx.recv(); 215 | if let Ok(op) = op { 216 | match op { 217 | SecretOp::Update(path) | SecretOp::Create(path) => { 218 | let src_path = &path.path; 219 | let dst_path = secret_src_to_dst_path(&src_prefix, &dst_prefix, &src_path); 220 | let src_secret: Result = { 221 | let mut client = src_client.lock().unwrap(); 222 | client.secret_backend(&path.mount); 223 | client.get_custom_secret(&src_path) 224 | }; 225 | let dst_secret: Result = { 226 | let mut client = dst_client.lock().unwrap(); 227 | client.secret_backend(mount_map[path.mount.as_str()]); 228 | client.get_custom_secret(&dst_path) 229 | }; 230 | if let Err(error) = src_secret { 231 | warn!("Failed to get secret {}: {}", &src_path, error); 232 | continue; 233 | } 234 | let src_secret = src_secret.unwrap(); 235 | if let Ok(dst_secret) = dst_secret { 236 | if dst_secret == src_secret { 237 | continue; 238 | } 239 | } 240 | info!("Creating/updating secret {}", &dst_path); 241 | if !dry_run { 242 | let result = { 243 | let client = dst_client.lock().unwrap(); 244 | client.set_custom_secret(&dst_path, &src_secret) 245 | }; 246 | if let Err(error) = result { 247 | warn!("Failed to set secret {}: {}", &dst_path, error); 248 | } else { 249 | stats.updated += 1; 250 | } 251 | } 252 | }, 253 | SecretOp::Delete(path) => { 254 | let secret = secret_src_to_dst_path(&src_prefix, &dst_prefix, &path.path); 255 | info!("Deleting secret {}", &secret); 256 | if !dry_run { 257 | let mut client = dst_client.lock().unwrap(); 258 | client.secret_backend(mount_map[path.mount.as_str()]); 259 | let _ = client.delete_secret(&path.path); 260 | } else { 261 | stats.deleted += 1; 262 | } 263 | }, 264 | SecretOp::FullSyncFinished => { 265 | info!("Secrets created/updated: {}, deleted: {}", &stats.updated, &stats.deleted); 266 | stats.reset(); 267 | if run_once { 268 | break; 269 | } 270 | }, 271 | } 272 | } 273 | } 274 | } 275 | 276 | 277 | // Convert AuditLog to SecretOp 278 | fn audit_log_op(mounts: &Vec, prefix: &str, version: &EngineVersion, log: &audit::AuditLog) -> Option { 279 | if log.log_type != "response" { 280 | return None; 281 | } 282 | if log.request.mount_type.is_none() { 283 | return None; 284 | } 285 | if log.request.mount_type != Some("kv".to_string()) { 286 | return None; 287 | } 288 | 289 | let operation = log.request.operation.clone(); 290 | if operation != "create" && operation != "update" && operation != "delete" { 291 | return None; 292 | } 293 | 294 | let path = match version { 295 | EngineVersion::V1 => secret_path_v1(&log.request.path), 296 | EngineVersion::V2 => secret_path_v2(&log.request.path), 297 | }; 298 | if let Some(path) = path { 299 | if !mounts.contains(&path.0) { 300 | return None; 301 | } 302 | if !path.1.starts_with(prefix) { 303 | return None; 304 | } 305 | if operation == "create" { 306 | return Some(SecretOp::Create(SecretPath {mount: path.0, path: path.1 })); 307 | } else if operation == "update" { 308 | return Some(SecretOp::Update(SecretPath {mount: path.0, path: path.1 })); 309 | } else if operation == "delete" { 310 | return Some(SecretOp::Delete(SecretPath {mount: path.0, path: path.1 })); 311 | } 312 | } 313 | None 314 | } 315 | 316 | // Convert Vault path to a secret path for KV v1 317 | // Example: "secret/path/to/secret" -> "secret", "path/to/secret" 318 | fn secret_path_v1(path: &str) -> Option<(String, String)> { 319 | let parts: Vec<&str> = path.split("/").collect(); 320 | if parts.len() < 2 { 321 | return None 322 | } 323 | Some((parts[0].to_string(), parts[1..].join("/"))) 324 | } 325 | 326 | // Convert Vault path to a secret path for KV v2 327 | // Example: "secret/data/path/to/secret" -> "secret", "path/to/secret" 328 | fn secret_path_v2(path: &str) -> Option<(String, String)> { 329 | let parts: Vec<&str> = path.split("/").collect(); 330 | if parts.len() < 3 { 331 | return None 332 | } 333 | // `vault kv metadata delete secret/path` has `metadata` instead of `data`, 334 | // we do not support this yet 335 | if parts[1] == "data" { 336 | Some((parts[0].to_string(), parts[2..].join("/"))) 337 | } else { 338 | None 339 | } 340 | } 341 | 342 | fn normalize_prefix(prefix: &str) -> String { 343 | if prefix.len() == 0 { 344 | return "".to_string(); 345 | } 346 | if prefix.ends_with("/") { 347 | prefix.to_string() 348 | } else { 349 | format!("{}/", prefix) 350 | } 351 | } 352 | 353 | // Convert source secret path to destination secret path. Prefixes must be normalized! 354 | // Example: "src/secret1" -> "dst/secret2" 355 | fn secret_src_to_dst_path(src_prefix: &str, dst_prefix: &str, path: &str) -> String { 356 | let mut path = path.to_string(); 357 | if src_prefix.len() > 0 { 358 | path = path.trim_start_matches(src_prefix).to_string(); 359 | } 360 | format!("{}{}", dst_prefix, &path) 361 | } 362 | 363 | #[cfg(test)] 364 | mod tests { 365 | use crate::sync::{normalize_prefix, secret_path_v1, secret_path_v2, secret_src_to_dst_path}; 366 | 367 | #[test] 368 | fn test_secret_path_v1_matches() { 369 | let path = "secret/path/to/secret"; 370 | let path = secret_path_v1(&path).unwrap(); 371 | assert_eq!(path.0, "secret"); 372 | assert_eq!(path.1, "path/to/secret"); 373 | } 374 | 375 | #[test] 376 | fn test_custom_secret_path_v1_matches() { 377 | let path = "custom/path/to/secret"; 378 | let path = secret_path_v1(&path).unwrap(); 379 | assert_eq!(path.0, "custom"); 380 | assert_eq!(path.1, "path/to/secret"); 381 | } 382 | 383 | #[test] 384 | fn test_secret_path_v1_not_matches() { 385 | let path = "secret"; 386 | let path = secret_path_v1(&path); 387 | assert_eq!(path.is_none(), true); 388 | } 389 | 390 | #[test] 391 | fn test_secret_path_v2_matches() { 392 | let path = "secret/data/path/to/secret"; 393 | let path = secret_path_v2(&path).unwrap(); 394 | assert_eq!(path.0, "secret"); 395 | assert_eq!(path.1, "path/to/secret"); 396 | } 397 | 398 | #[test] 399 | fn test_custom_secret_path_v2_matches() { 400 | let path = "custom/data/path/to/secret"; 401 | let path = secret_path_v2(&path).unwrap(); 402 | assert_eq!(path.0, "custom"); 403 | assert_eq!(path.1, "path/to/secret"); 404 | } 405 | 406 | #[test] 407 | fn test_secret_path_v2_not_matches() { 408 | let path = "secret/metadata/path/to/secret"; 409 | let path = secret_path_v2(&path); 410 | assert_eq!(path.is_none(), true); 411 | } 412 | 413 | #[test] 414 | fn test_normalize_prefix() { 415 | assert_eq!(normalize_prefix(""), ""); 416 | assert_eq!(normalize_prefix("src"), "src/"); 417 | assert_eq!(normalize_prefix("src/"), "src/"); 418 | } 419 | 420 | #[test] 421 | fn test_secret_src_to_dst_path() { 422 | assert_eq!(secret_src_to_dst_path("src/", "dst/", "src/secret"), "dst/secret"); 423 | assert_eq!(secret_src_to_dst_path("", "dst/", "src/secret"), "dst/src/secret"); 424 | assert_eq!(secret_src_to_dst_path("", "", "src/secret"), "src/secret"); 425 | } 426 | 427 | } 428 | -------------------------------------------------------------------------------- /scripts/test-sync.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Local test for vault-sync. Requires installed vault. 4 | 5 | set -e -o pipefail 6 | 7 | : ${VAULT_SYNC_BINARY:="cargo run --"} 8 | 9 | VAULT_VERSION="$(vault version)" 10 | echo "$VAULT_VERSION" 11 | 12 | VAULT_ARGS=( 13 | server 14 | -dev 15 | -dev-root-token-id=unsafe-root-token 16 | ) 17 | 18 | # Allow creating audit devices via the API. 19 | # This is required for newer versions of OpenBao, will no work with old versions. 20 | if [[ $VAULT_VERSION = *OpenBao* ]]; then 21 | echo "unsafe_allow_api_audit_creation = true" > openbao.hcl 22 | VAULT_ARGS+=( 23 | -config=openbao.hcl 24 | ) 25 | fi 26 | 27 | vault "${VAULT_ARGS[@]}" &> vault.log & 28 | echo $! > vault.pid 29 | 30 | function cleanup() {( 31 | set -e 32 | if [[ -f vault.pid ]]; then 33 | kill $( /tmp/vault-sync-token.env 118 | export VAULT_SYNC_SRC_TOKEN=unsafe-root-token 119 | export VAULT_SYNC_DST_TOKEN=unsafe-root-token 120 | EOF 121 | 122 | cat < /tmp/vault-sync-app-role.env 123 | export VAULT_SYNC_SRC_ROLE_ID=$(vault read auth/approle/role/vault-sync-reader/role-id -format=json | jq -r .data.role_id) 124 | export VAULT_SYNC_SRC_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-reader/secret-id -format=json | jq -r .data.secret_id) 125 | export VAULT_SYNC_DST_ROLE_ID=$(vault read auth/approle/role/vault-sync-writer/role-id -format=json | jq -r .data.role_id) 126 | export VAULT_SYNC_DST_SECRET_ID=$(vault write -f auth/approle/role/vault-sync-writer/secret-id -format=json | jq -r .data.secret_id) 127 | EOF 128 | 129 | function test_token {( 130 | local src_backend=$1 131 | local dst_backend=${2:-$src_backend} 132 | local secret_name=test-$RANDOM 133 | 134 | if [[ $src_namespace ]]; then 135 | namespace="-namespace $src_namespace" 136 | else 137 | namespace="" 138 | fi 139 | 140 | vault kv put $namespace -mount $src_backend ${src_prefix}${secret_name} foo=bar 141 | 142 | source /tmp/vault-sync-token.env 143 | $VAULT_SYNC_BINARY --config /tmp/vault-sync.yaml --once 144 | 145 | if [[ $dst_namespace ]]; then 146 | namespace="-namespace $dst_namespace" 147 | else 148 | namespace="" 149 | fi 150 | 151 | vault kv get $namespace -mount $dst_backend ${dst_prefix}${secret_name} 152 | vault kv get $namespace -mount $dst_backend ${dst_prefix}${secret_name} | grep -qE '^foo\s+bar$' 153 | )} 154 | 155 | function test_app_role {( 156 | local src_backend=$1 157 | local dst_backend=${2:-$src_backend} 158 | local secret_name=test-$RANDOM 159 | 160 | vault kv put -mount $src_backend ${src_prefix}${secret_name} foo=bar 161 | 162 | source /tmp/vault-sync-app-role.env 163 | $VAULT_SYNC_BINARY --config /tmp/vault-sync.yaml --once 164 | vault kv get -mount $dst_backend ${dst_prefix}${secret_name} 165 | vault kv get -mount $dst_backend ${dst_prefix}${secret_name} | grep -qE '^foo\s+bar$' 166 | )} 167 | 168 | function test_token_with_audit_device {( 169 | local src_backend=$1 170 | local dst_backend=${2:-$src_backend} 171 | local secret_name=test-$RANDOM 172 | local audit_device_name=vault-sync-$src_backend-$dst_backend 173 | 174 | source /tmp/vault-sync-token.env 175 | 176 | vault kv put -mount $src_backend ${src_prefix}${secret_name}-1 foo=bar 177 | 178 | $VAULT_SYNC_BINARY --config /tmp/vault-sync.yaml & 179 | echo $! > vault-sync.pid 180 | 181 | echo Wating for vault-sync to start and make the initial sync ... 182 | VAULT_SYNC_READY="" 183 | for i in 1 2 3 4 5; do 184 | if vault kv get -mount $dst_backend ${dst_prefix}${secret_name}-1 2> /dev/null; then 185 | vault kv get -mount $dst_backend ${dst_prefix}${secret_name}-1 | grep -qE '^foo\s+bar$' 186 | VAULT_SYNC_READY="true" 187 | break 188 | fi 189 | sleep 1 190 | done 191 | if [[ ! $VAULT_SYNC_READY ]]; then 192 | echo "vault-sync failed to start with audit device" 193 | exit 1 194 | fi 195 | 196 | # Enable audit device that sends log to vault-sync 197 | vault audit enable -path $audit_device_name socket socket_type=tcp address=127.0.0.1:8202 198 | 199 | vault kv put -mount $src_backend ${dst_prefix}${secret_name}-2 foo=bar 200 | 201 | echo Wating for vault-sync to sync on event ... 202 | VAULT_SYNC_READY="" 203 | for i in 1 2 3 4 5; do 204 | if vault kv get -mount $dst_backend ${dst_prefix}/${secret_name}-2 2> /dev/null; then 205 | vault kv get -mount $dst_backend ${dst_prefix}/${secret_name}-2 | grep -qE '^foo\s+bar$' 206 | VAULT_SYNC_READY="true" 207 | break 208 | fi 209 | sleep 1 210 | done 211 | if [[ ! $VAULT_SYNC_READY ]]; then 212 | echo "vault-sync failed to sync on the event from the audit device" 213 | exit 1 214 | fi 215 | 216 | vault audit disable $audit_device_name 217 | 218 | kill $( vault-sync.pid 233 | 234 | echo Wating for vault-sync to start and make the initial sync ... 235 | VAULT_SYNC_READY1="" 236 | VAULT_SYNC_READY2="" 237 | for i in 1 2 3 4 5; do 238 | if vault kv get -mount secret21 ${secret_name}-1 &> /dev/null; then 239 | vault kv get -mount secret21 ${secret_name}-1 | grep -qE '^foo\s+bar$' 240 | VAULT_SYNC_READY1="true" 241 | fi 242 | if vault kv get -mount secret22 ${secret_name}-1 &> /dev/null; then 243 | vault kv get -mount secret22 ${secret_name}-1 | grep -qE '^foo\s+bar$' 244 | VAULT_SYNC_READY2="true" 245 | fi 246 | sleep 1 247 | done 248 | if [[ ! $VAULT_SYNC_READY1 || ! $VAULT_SYNC_READY1 ]]; then 249 | echo "vault-sync failed to start with audit device" 250 | exit 1 251 | fi 252 | 253 | # Enable audit device that sends log to vault-sync 254 | vault audit enable -path $audit_device_name socket socket_type=tcp address=127.0.0.1:8202 255 | 256 | vault kv put -mount secret11 ${secret_name}-2 foo=bar 257 | vault kv put -mount secret12 ${secret_name}-2 foo=bar 258 | 259 | echo Wating for vault-sync to sync on event ... 260 | VAULT_SYNC_READY1="" 261 | VAULT_SYNC_READY2="" 262 | for i in 1 2 3 4 5; do 263 | if vault kv get -mount secret21 ${secret_name}-2 &> /dev/null; then 264 | vault kv get -mount secret21 ${secret_name}-2 | grep -qE '^foo\s+bar$' 265 | VAULT_SYNC_READY1="true" 266 | fi 267 | if vault kv get -mount secret22 ${secret_name}-2 &> /dev/null; then 268 | vault kv get -mount secret22 ${secret_name}-2 | grep -qE '^foo\s+bar$' 269 | VAULT_SYNC_READY2="true" 270 | fi 271 | sleep 1 272 | done 273 | if [[ ! $VAULT_SYNC_READY1 || ! $VAULT_SYNC_READY2 ]]; then 274 | echo "vault-sync failed to sync on the event from the audit device" 275 | exit 1 276 | fi 277 | 278 | vault audit disable $audit_device_name 279 | 280 | kill $( secret/dst 285 | cat < /tmp/vault-sync.yaml 286 | id: vault-sync 287 | full_sync_interval: 10 288 | src: 289 | url: http://127.0.0.1:8200/ 290 | prefix: src 291 | dst: 292 | url: http://127.0.0.1:8200/ 293 | prefix: dst 294 | EOF 295 | 296 | src_prefix="src/" 297 | dst_prefix="dst/" 298 | 299 | test_token secret 300 | test_app_role secret 301 | 302 | # secret1/src -> secret1/dst 303 | cat < /tmp/vault-sync.yaml 304 | id: vault-sync 305 | full_sync_interval: 10 306 | src: 307 | url: http://127.0.0.1:8200/ 308 | prefix: src 309 | backend: secret1 310 | version: 1 311 | dst: 312 | url: http://127.0.0.1:8200/ 313 | prefix: dst 314 | backend: secret1 315 | version: 1 316 | EOF 317 | 318 | src_prefix="src/" 319 | dst_prefix="dst/" 320 | 321 | test_token secret1 322 | test_app_role secret1 323 | 324 | # secret2/src -> secret2/dst 325 | cat < /tmp/vault-sync.yaml 326 | id: vault-sync 327 | full_sync_interval: 10 328 | src: 329 | url: http://127.0.0.1:8200/ 330 | prefix: src 331 | backend: secret2 332 | dst: 333 | url: http://127.0.0.1:8200/ 334 | prefix: dst 335 | backend: secret2 336 | EOF 337 | 338 | src_prefix="src/" 339 | dst_prefix="dst/" 340 | 341 | test_token secret2 342 | test_app_role secret2 343 | 344 | # secret1/src -> secret2/dst 345 | cat < /tmp/vault-sync.yaml 346 | id: vault-sync 347 | full_sync_interval: 10 348 | src: 349 | url: http://127.0.0.1:8200/ 350 | prefix: src 351 | backend: secret1 352 | version: 1 353 | dst: 354 | url: http://127.0.0.1:8200/ 355 | prefix: dst 356 | backend: secret2 357 | EOF 358 | 359 | src_prefix="src/" 360 | dst_prefix="dst/" 361 | 362 | test_token secret1 secret2 363 | test_app_role secret1 secret2 364 | 365 | # secret2/src -> secret1/dst 366 | cat < /tmp/vault-sync.yaml 367 | id: vault-sync 368 | full_sync_interval: 10 369 | src: 370 | url: http://127.0.0.1:8200/ 371 | prefix: src 372 | backend: secret2 373 | dst: 374 | url: http://127.0.0.1:8200/ 375 | prefix: dst 376 | backend: secret1 377 | version: 1 378 | EOF 379 | 380 | src_prefix="src/" 381 | dst_prefix="dst/" 382 | 383 | test_token secret2 secret1 384 | test_app_role secret2 secret1 385 | 386 | # secret1 -> secret2 387 | cat < /tmp/vault-sync.yaml 388 | id: vault-sync 389 | full_sync_interval: 10 390 | src: 391 | url: http://127.0.0.1:8200/ 392 | backend: secret1 393 | version: 1 394 | dst: 395 | url: http://127.0.0.1:8200/ 396 | backend: secret2 397 | EOF 398 | 399 | src_prefix="" 400 | dst_prefix="" 401 | 402 | test_token secret1 secret2 403 | test_app_role secret1 secret2 404 | 405 | # secret2 -> secret1 406 | cat < /tmp/vault-sync.yaml 407 | id: vault-sync 408 | full_sync_interval: 10 409 | src: 410 | url: http://127.0.0.1:8200/ 411 | backend: secret2 412 | dst: 413 | url: http://127.0.0.1:8200/ 414 | backend: secret1 415 | version: 1 416 | EOF 417 | 418 | src_prefix="" 419 | dst_prefix="" 420 | 421 | test_token secret2 secret1 422 | test_app_role secret2 secret1 423 | 424 | # Enable audit device that always works 425 | vault audit enable -path vault-audit file file_path=vault-audit.log 426 | 427 | # secret/src -> secret/dst 428 | cat < /tmp/vault-sync.yaml 429 | id: vault-sync-secret 430 | bind: 0.0.0.0:8202 431 | full_sync_interval: 60 432 | src: 433 | url: http://127.0.0.1:8200/ 434 | prefix: src 435 | dst: 436 | url: http://127.0.0.1:8200/ 437 | prefix: dst 438 | EOF 439 | 440 | src_prefix="src/" 441 | dst_prefix="dst/" 442 | 443 | test_token_with_audit_device secret 444 | 445 | # secret1/src -> secret1/dst 446 | cat < /tmp/vault-sync.yaml 447 | id: vault-sync-secret 448 | full_sync_interval: 60 449 | bind: 0.0.0.0:8202 450 | src: 451 | url: http://127.0.0.1:8200/ 452 | prefix: src 453 | backend: secret1 454 | version: 1 455 | dst: 456 | url: http://127.0.0.1:8200/ 457 | prefix: dst 458 | backend: secret1 459 | version: 1 460 | EOF 461 | 462 | src_prefix="src/" 463 | dst_prefix="dst/" 464 | 465 | test_token_with_audit_device secret1 466 | 467 | # secret2/src -> secret2/dst 468 | cat < /tmp/vault-sync.yaml 469 | id: vault-sync-secret 470 | full_sync_interval: 60 471 | bind: 0.0.0.0:8202 472 | src: 473 | url: http://127.0.0.1:8200/ 474 | prefix: src 475 | backend: secret2 476 | dst: 477 | url: http://127.0.0.1:8200/ 478 | prefix: dst 479 | backend: secret2 480 | EOF 481 | 482 | src_prefix="src/" 483 | dst_prefix="dst/" 484 | 485 | test_token_with_audit_device secret2 486 | 487 | # secret1 -> secret2 488 | cat < /tmp/vault-sync.yaml 489 | id: vault-sync-secret 490 | full_sync_interval: 60 491 | bind: 0.0.0.0:8202 492 | src: 493 | url: http://127.0.0.1:8200/ 494 | backend: secret1 495 | version: 1 496 | dst: 497 | url: http://127.0.0.1:8200/ 498 | backend: secret2 499 | EOF 500 | 501 | src_prefix="" 502 | dst_prefix="" 503 | 504 | test_token_with_audit_device secret1 secret2 505 | 506 | # secret2 -> secret1 507 | cat < /tmp/vault-sync.yaml 508 | id: vault-sync-secret 509 | full_sync_interval: 60 510 | bind: 0.0.0.0:8202 511 | src: 512 | url: http://127.0.0.1:8200/ 513 | backend: secret2 514 | dst: 515 | url: http://127.0.0.1:8200/ 516 | backend: secret1 517 | version: 1 518 | EOF 519 | 520 | src_prefix="" 521 | dst_prefix="" 522 | 523 | test_token_with_audit_device secret2 secret1 524 | 525 | # secret11, secret12 -> secret21, secret22 526 | cat < /tmp/vault-sync.yaml 527 | id: vault-sync-secret 528 | full_sync_interval: 60 529 | bind: 0.0.0.0:8202 530 | src: 531 | url: http://127.0.0.1:8200/ 532 | backends: 533 | - secret11 534 | - secret12 535 | dst: 536 | url: http://127.0.0.1:8200/ 537 | backends: 538 | - secret21 539 | - secret22 540 | EOF 541 | 542 | test_multiple_backends 543 | 544 | if [[ ! " $@ " =~ " --namespaces " ]]; then 545 | exit 0 546 | fi 547 | 548 | vault namespace create ns1 549 | vault namespace create ns2 550 | vault namespace create -namespace=ns1 ns11 551 | vault namespace create -namespace=ns2 ns21 552 | 553 | vault secrets enable -namespace=ns1 -version=2 -path=secret kv 554 | vault secrets enable -namespace=ns2 -version=2 -path=secret kv 555 | vault secrets enable -namespace=ns1/ns11 -version=2 -path=secret kv 556 | vault secrets enable -namespace=ns2/ns21 -version=2 -path=secret kv 557 | 558 | # ns1/secret -> secret 559 | cat < /tmp/vault-sync.yaml 560 | id: vault-sync 561 | full_sync_interval: 10 562 | src: 563 | url: http://127.0.0.1:8200/ 564 | namespace: ns1 565 | dst: 566 | url: http://127.0.0.1:8200/ 567 | EOF 568 | 569 | src_namespace="ns1" 570 | dst_namespace="" 571 | src_prefix="" 572 | dst_prefix="" 573 | 574 | test_token secret 575 | 576 | # secret -> ns2/secret 577 | cat < /tmp/vault-sync.yaml 578 | id: vault-sync 579 | full_sync_interval: 10 580 | src: 581 | url: http://127.0.0.1:8200/ 582 | dst: 583 | url: http://127.0.0.1:8200/ 584 | namespace: ns2 585 | EOF 586 | 587 | src_namespace="" 588 | dst_namespace="ns2" 589 | src_prefix="" 590 | dst_prefix="" 591 | 592 | test_token secret 593 | 594 | # ns1/secret -> ns2/secret 595 | cat < /tmp/vault-sync.yaml 596 | id: vault-sync 597 | full_sync_interval: 10 598 | src: 599 | url: http://127.0.0.1:8200/ 600 | namespace: ns1 601 | dst: 602 | url: http://127.0.0.1:8200/ 603 | namespace: ns2 604 | EOF 605 | 606 | src_namespace="ns1" 607 | dst_namespace="ns2" 608 | src_prefix="" 609 | dst_prefix="" 610 | 611 | test_token secret 612 | 613 | # ns1/ns11/secret -> ns2/ns21/secret 614 | cat < /tmp/vault-sync.yaml 615 | id: vault-sync 616 | full_sync_interval: 10 617 | src: 618 | url: http://127.0.0.1:8200/ 619 | namespace: ns1/ns11 620 | dst: 621 | url: http://127.0.0.1:8200/ 622 | namespace: ns2/ns21 623 | EOF 624 | 625 | src_namespace="ns1/ns11" 626 | dst_namespace="ns2/ns21" 627 | src_prefix="" 628 | dst_prefix="" 629 | 630 | test_token secret 631 | 632 | # ns1/secret/src -> ns2/secret/dst 633 | cat < /tmp/vault-sync.yaml 634 | id: vault-sync 635 | full_sync_interval: 10 636 | src: 637 | url: http://127.0.0.1:8200/ 638 | namespace: ns1 639 | prefix: src 640 | dst: 641 | url: http://127.0.0.1:8200/ 642 | namespace: ns2 643 | prefix: dst 644 | EOF 645 | 646 | src_namespace="ns1" 647 | dst_namespace="ns2" 648 | src_prefix="src/" 649 | dst_prefix="dst/" 650 | 651 | test_token secret 652 | 653 | # ns1/secret/src -> ns2/ns21/secret 654 | cat < /tmp/vault-sync.yaml 655 | id: vault-sync 656 | full_sync_interval: 10 657 | src: 658 | url: http://127.0.0.1:8200/ 659 | namespace: ns1 660 | prefix: src 661 | dst: 662 | url: http://127.0.0.1:8200/ 663 | namespace: ns2/ns21 664 | EOF 665 | 666 | src_namespace="ns1" 667 | dst_namespace="ns2/ns21" 668 | src_prefix="src/" 669 | dst_prefix="" 670 | 671 | test_token secret 672 | 673 | # ns1/ns11/secret -> ns2/secret/dst 674 | cat < /tmp/vault-sync.yaml 675 | id: vault-sync 676 | full_sync_interval: 10 677 | src: 678 | url: http://127.0.0.1:8200/ 679 | namespace: ns1/ns11 680 | dst: 681 | url: http://127.0.0.1:8200/ 682 | namespace: ns2 683 | prefix: dst 684 | EOF 685 | 686 | src_namespace="ns1/ns11" 687 | dst_namespace="ns2" 688 | src_prefix="" 689 | dst_prefix="dst/" 690 | 691 | test_token secret 692 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "ansi_term" 37 | version = "0.12.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 40 | dependencies = [ 41 | "winapi", 42 | ] 43 | 44 | [[package]] 45 | name = "atty" 46 | version = "0.2.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 49 | dependencies = [ 50 | "hermit-abi", 51 | "libc", 52 | "winapi", 53 | ] 54 | 55 | [[package]] 56 | name = "autocfg" 57 | version = "1.4.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 60 | 61 | [[package]] 62 | name = "backtrace" 63 | version = "0.3.74" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 66 | dependencies = [ 67 | "addr2line", 68 | "cfg-if", 69 | "libc", 70 | "miniz_oxide", 71 | "object", 72 | "rustc-demangle", 73 | "windows-targets 0.52.6", 74 | ] 75 | 76 | [[package]] 77 | name = "base64" 78 | version = "0.13.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 81 | 82 | [[package]] 83 | name = "base64" 84 | version = "0.21.7" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 87 | 88 | [[package]] 89 | name = "bitflags" 90 | version = "1.3.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 93 | 94 | [[package]] 95 | name = "bitflags" 96 | version = "2.9.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 99 | 100 | [[package]] 101 | name = "bumpalo" 102 | version = "3.17.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 105 | 106 | [[package]] 107 | name = "bytes" 108 | version = "1.10.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 111 | 112 | [[package]] 113 | name = "cc" 114 | version = "1.2.16" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" 117 | dependencies = [ 118 | "shlex", 119 | ] 120 | 121 | [[package]] 122 | name = "cfg-if" 123 | version = "1.0.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 126 | 127 | [[package]] 128 | name = "cfg_aliases" 129 | version = "0.2.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 132 | 133 | [[package]] 134 | name = "chrono" 135 | version = "0.4.40" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 138 | dependencies = [ 139 | "android-tzdata", 140 | "iana-time-zone", 141 | "js-sys", 142 | "num-traits", 143 | "wasm-bindgen", 144 | "windows-link", 145 | ] 146 | 147 | [[package]] 148 | name = "clap" 149 | version = "2.34.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 152 | dependencies = [ 153 | "ansi_term", 154 | "atty", 155 | "bitflags 1.3.2", 156 | "strsim", 157 | "textwrap", 158 | "unicode-width", 159 | "vec_map", 160 | ] 161 | 162 | [[package]] 163 | name = "core-foundation" 164 | version = "0.9.4" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 167 | dependencies = [ 168 | "core-foundation-sys", 169 | "libc", 170 | ] 171 | 172 | [[package]] 173 | name = "core-foundation-sys" 174 | version = "0.8.7" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 177 | 178 | [[package]] 179 | name = "ctrlc" 180 | version = "3.4.5" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" 183 | dependencies = [ 184 | "nix", 185 | "windows-sys 0.59.0", 186 | ] 187 | 188 | [[package]] 189 | name = "deranged" 190 | version = "0.3.11" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 193 | dependencies = [ 194 | "powerfmt", 195 | ] 196 | 197 | [[package]] 198 | name = "displaydoc" 199 | version = "0.2.5" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 202 | dependencies = [ 203 | "proc-macro2", 204 | "quote", 205 | "syn", 206 | ] 207 | 208 | [[package]] 209 | name = "encoding_rs" 210 | version = "0.8.35" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 213 | dependencies = [ 214 | "cfg-if", 215 | ] 216 | 217 | [[package]] 218 | name = "equivalent" 219 | version = "1.0.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 222 | 223 | [[package]] 224 | name = "errno" 225 | version = "0.3.10" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 228 | dependencies = [ 229 | "libc", 230 | "windows-sys 0.59.0", 231 | ] 232 | 233 | [[package]] 234 | name = "fastrand" 235 | version = "2.3.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 238 | 239 | [[package]] 240 | name = "fnv" 241 | version = "1.0.7" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 244 | 245 | [[package]] 246 | name = "foreign-types" 247 | version = "0.3.2" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 250 | dependencies = [ 251 | "foreign-types-shared", 252 | ] 253 | 254 | [[package]] 255 | name = "foreign-types-shared" 256 | version = "0.1.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 259 | 260 | [[package]] 261 | name = "form_urlencoded" 262 | version = "1.2.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 265 | dependencies = [ 266 | "percent-encoding", 267 | ] 268 | 269 | [[package]] 270 | name = "futures-channel" 271 | version = "0.3.31" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 274 | dependencies = [ 275 | "futures-core", 276 | ] 277 | 278 | [[package]] 279 | name = "futures-core" 280 | version = "0.3.31" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 283 | 284 | [[package]] 285 | name = "futures-io" 286 | version = "0.3.31" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 289 | 290 | [[package]] 291 | name = "futures-sink" 292 | version = "0.3.31" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 295 | 296 | [[package]] 297 | name = "futures-task" 298 | version = "0.3.31" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 301 | 302 | [[package]] 303 | name = "futures-util" 304 | version = "0.3.31" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 307 | dependencies = [ 308 | "futures-core", 309 | "futures-io", 310 | "futures-task", 311 | "memchr", 312 | "pin-project-lite", 313 | "pin-utils", 314 | "slab", 315 | ] 316 | 317 | [[package]] 318 | name = "getrandom" 319 | version = "0.3.1" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 322 | dependencies = [ 323 | "cfg-if", 324 | "libc", 325 | "wasi 0.13.3+wasi-0.2.2", 326 | "windows-targets 0.52.6", 327 | ] 328 | 329 | [[package]] 330 | name = "gimli" 331 | version = "0.31.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 334 | 335 | [[package]] 336 | name = "h2" 337 | version = "0.3.26" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 340 | dependencies = [ 341 | "bytes", 342 | "fnv", 343 | "futures-core", 344 | "futures-sink", 345 | "futures-util", 346 | "http", 347 | "indexmap", 348 | "slab", 349 | "tokio", 350 | "tokio-util", 351 | "tracing", 352 | ] 353 | 354 | [[package]] 355 | name = "hashbrown" 356 | version = "0.15.2" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 359 | 360 | [[package]] 361 | name = "hashicorp_vault" 362 | version = "2.1.1" 363 | dependencies = [ 364 | "base64 0.13.1", 365 | "chrono", 366 | "log", 367 | "quick-error", 368 | "reqwest", 369 | "serde", 370 | "serde_derive", 371 | "serde_json", 372 | "url", 373 | ] 374 | 375 | [[package]] 376 | name = "hermit-abi" 377 | version = "0.1.19" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 380 | dependencies = [ 381 | "libc", 382 | ] 383 | 384 | [[package]] 385 | name = "http" 386 | version = "0.2.12" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 389 | dependencies = [ 390 | "bytes", 391 | "fnv", 392 | "itoa", 393 | ] 394 | 395 | [[package]] 396 | name = "http-body" 397 | version = "0.4.6" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 400 | dependencies = [ 401 | "bytes", 402 | "http", 403 | "pin-project-lite", 404 | ] 405 | 406 | [[package]] 407 | name = "httparse" 408 | version = "1.10.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 411 | 412 | [[package]] 413 | name = "httpdate" 414 | version = "1.0.3" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 417 | 418 | [[package]] 419 | name = "hyper" 420 | version = "0.14.32" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 423 | dependencies = [ 424 | "bytes", 425 | "futures-channel", 426 | "futures-core", 427 | "futures-util", 428 | "h2", 429 | "http", 430 | "http-body", 431 | "httparse", 432 | "httpdate", 433 | "itoa", 434 | "pin-project-lite", 435 | "socket2", 436 | "tokio", 437 | "tower-service", 438 | "tracing", 439 | "want", 440 | ] 441 | 442 | [[package]] 443 | name = "hyper-tls" 444 | version = "0.5.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 447 | dependencies = [ 448 | "bytes", 449 | "hyper", 450 | "native-tls", 451 | "tokio", 452 | "tokio-native-tls", 453 | ] 454 | 455 | [[package]] 456 | name = "iana-time-zone" 457 | version = "0.1.61" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 460 | dependencies = [ 461 | "android_system_properties", 462 | "core-foundation-sys", 463 | "iana-time-zone-haiku", 464 | "js-sys", 465 | "wasm-bindgen", 466 | "windows-core", 467 | ] 468 | 469 | [[package]] 470 | name = "iana-time-zone-haiku" 471 | version = "0.1.2" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 474 | dependencies = [ 475 | "cc", 476 | ] 477 | 478 | [[package]] 479 | name = "icu_collections" 480 | version = "1.5.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 483 | dependencies = [ 484 | "displaydoc", 485 | "yoke", 486 | "zerofrom", 487 | "zerovec", 488 | ] 489 | 490 | [[package]] 491 | name = "icu_locid" 492 | version = "1.5.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 495 | dependencies = [ 496 | "displaydoc", 497 | "litemap", 498 | "tinystr", 499 | "writeable", 500 | "zerovec", 501 | ] 502 | 503 | [[package]] 504 | name = "icu_locid_transform" 505 | version = "1.5.0" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 508 | dependencies = [ 509 | "displaydoc", 510 | "icu_locid", 511 | "icu_locid_transform_data", 512 | "icu_provider", 513 | "tinystr", 514 | "zerovec", 515 | ] 516 | 517 | [[package]] 518 | name = "icu_locid_transform_data" 519 | version = "1.5.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 522 | 523 | [[package]] 524 | name = "icu_normalizer" 525 | version = "1.5.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 528 | dependencies = [ 529 | "displaydoc", 530 | "icu_collections", 531 | "icu_normalizer_data", 532 | "icu_properties", 533 | "icu_provider", 534 | "smallvec", 535 | "utf16_iter", 536 | "utf8_iter", 537 | "write16", 538 | "zerovec", 539 | ] 540 | 541 | [[package]] 542 | name = "icu_normalizer_data" 543 | version = "1.5.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 546 | 547 | [[package]] 548 | name = "icu_properties" 549 | version = "1.5.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 552 | dependencies = [ 553 | "displaydoc", 554 | "icu_collections", 555 | "icu_locid_transform", 556 | "icu_properties_data", 557 | "icu_provider", 558 | "tinystr", 559 | "zerovec", 560 | ] 561 | 562 | [[package]] 563 | name = "icu_properties_data" 564 | version = "1.5.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 567 | 568 | [[package]] 569 | name = "icu_provider" 570 | version = "1.5.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 573 | dependencies = [ 574 | "displaydoc", 575 | "icu_locid", 576 | "icu_provider_macros", 577 | "stable_deref_trait", 578 | "tinystr", 579 | "writeable", 580 | "yoke", 581 | "zerofrom", 582 | "zerovec", 583 | ] 584 | 585 | [[package]] 586 | name = "icu_provider_macros" 587 | version = "1.5.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 590 | dependencies = [ 591 | "proc-macro2", 592 | "quote", 593 | "syn", 594 | ] 595 | 596 | [[package]] 597 | name = "idna" 598 | version = "1.0.3" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 601 | dependencies = [ 602 | "idna_adapter", 603 | "smallvec", 604 | "utf8_iter", 605 | ] 606 | 607 | [[package]] 608 | name = "idna_adapter" 609 | version = "1.2.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 612 | dependencies = [ 613 | "icu_normalizer", 614 | "icu_properties", 615 | ] 616 | 617 | [[package]] 618 | name = "indexmap" 619 | version = "2.7.1" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 622 | dependencies = [ 623 | "equivalent", 624 | "hashbrown", 625 | ] 626 | 627 | [[package]] 628 | name = "ipnet" 629 | version = "2.11.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 632 | 633 | [[package]] 634 | name = "itoa" 635 | version = "1.0.14" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 638 | 639 | [[package]] 640 | name = "js-sys" 641 | version = "0.3.77" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 644 | dependencies = [ 645 | "once_cell", 646 | "wasm-bindgen", 647 | ] 648 | 649 | [[package]] 650 | name = "libc" 651 | version = "0.2.170" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 654 | 655 | [[package]] 656 | name = "linux-raw-sys" 657 | version = "0.4.15" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 660 | 661 | [[package]] 662 | name = "litemap" 663 | version = "0.7.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 666 | 667 | [[package]] 668 | name = "log" 669 | version = "0.4.26" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 672 | 673 | [[package]] 674 | name = "memchr" 675 | version = "2.7.4" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 678 | 679 | [[package]] 680 | name = "mime" 681 | version = "0.3.17" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 684 | 685 | [[package]] 686 | name = "miniz_oxide" 687 | version = "0.8.5" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 690 | dependencies = [ 691 | "adler2", 692 | ] 693 | 694 | [[package]] 695 | name = "mio" 696 | version = "1.0.3" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 699 | dependencies = [ 700 | "libc", 701 | "wasi 0.11.0+wasi-snapshot-preview1", 702 | "windows-sys 0.52.0", 703 | ] 704 | 705 | [[package]] 706 | name = "native-tls" 707 | version = "0.2.14" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 710 | dependencies = [ 711 | "libc", 712 | "log", 713 | "openssl", 714 | "openssl-probe", 715 | "openssl-sys", 716 | "schannel", 717 | "security-framework", 718 | "security-framework-sys", 719 | "tempfile", 720 | ] 721 | 722 | [[package]] 723 | name = "nix" 724 | version = "0.29.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 727 | dependencies = [ 728 | "bitflags 2.9.0", 729 | "cfg-if", 730 | "cfg_aliases", 731 | "libc", 732 | ] 733 | 734 | [[package]] 735 | name = "num-conv" 736 | version = "0.1.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 739 | 740 | [[package]] 741 | name = "num-traits" 742 | version = "0.2.19" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 745 | dependencies = [ 746 | "autocfg", 747 | ] 748 | 749 | [[package]] 750 | name = "num_threads" 751 | version = "0.1.7" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 754 | dependencies = [ 755 | "libc", 756 | ] 757 | 758 | [[package]] 759 | name = "object" 760 | version = "0.36.7" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 763 | dependencies = [ 764 | "memchr", 765 | ] 766 | 767 | [[package]] 768 | name = "once_cell" 769 | version = "1.20.3" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 772 | 773 | [[package]] 774 | name = "openssl" 775 | version = "0.10.71" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" 778 | dependencies = [ 779 | "bitflags 2.9.0", 780 | "cfg-if", 781 | "foreign-types", 782 | "libc", 783 | "once_cell", 784 | "openssl-macros", 785 | "openssl-sys", 786 | ] 787 | 788 | [[package]] 789 | name = "openssl-macros" 790 | version = "0.1.1" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 793 | dependencies = [ 794 | "proc-macro2", 795 | "quote", 796 | "syn", 797 | ] 798 | 799 | [[package]] 800 | name = "openssl-probe" 801 | version = "0.1.6" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 804 | 805 | [[package]] 806 | name = "openssl-sys" 807 | version = "0.9.106" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" 810 | dependencies = [ 811 | "cc", 812 | "libc", 813 | "pkg-config", 814 | "vcpkg", 815 | ] 816 | 817 | [[package]] 818 | name = "percent-encoding" 819 | version = "2.3.1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 822 | 823 | [[package]] 824 | name = "pin-project-lite" 825 | version = "0.2.16" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 828 | 829 | [[package]] 830 | name = "pin-utils" 831 | version = "0.1.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 834 | 835 | [[package]] 836 | name = "pkg-config" 837 | version = "0.3.31" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 840 | 841 | [[package]] 842 | name = "powerfmt" 843 | version = "0.2.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 846 | 847 | [[package]] 848 | name = "proc-macro2" 849 | version = "1.0.93" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 852 | dependencies = [ 853 | "unicode-ident", 854 | ] 855 | 856 | [[package]] 857 | name = "quick-error" 858 | version = "2.0.1" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 861 | 862 | [[package]] 863 | name = "quote" 864 | version = "1.0.38" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 867 | dependencies = [ 868 | "proc-macro2", 869 | ] 870 | 871 | [[package]] 872 | name = "reqwest" 873 | version = "0.11.27" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 876 | dependencies = [ 877 | "base64 0.21.7", 878 | "bytes", 879 | "encoding_rs", 880 | "futures-core", 881 | "futures-util", 882 | "h2", 883 | "http", 884 | "http-body", 885 | "hyper", 886 | "hyper-tls", 887 | "ipnet", 888 | "js-sys", 889 | "log", 890 | "mime", 891 | "native-tls", 892 | "once_cell", 893 | "percent-encoding", 894 | "pin-project-lite", 895 | "rustls-pemfile", 896 | "serde", 897 | "serde_json", 898 | "serde_urlencoded", 899 | "sync_wrapper", 900 | "system-configuration", 901 | "tokio", 902 | "tokio-native-tls", 903 | "tower-service", 904 | "url", 905 | "wasm-bindgen", 906 | "wasm-bindgen-futures", 907 | "web-sys", 908 | "winreg", 909 | ] 910 | 911 | [[package]] 912 | name = "rustc-demangle" 913 | version = "0.1.24" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 916 | 917 | [[package]] 918 | name = "rustix" 919 | version = "0.38.44" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 922 | dependencies = [ 923 | "bitflags 2.9.0", 924 | "errno", 925 | "libc", 926 | "linux-raw-sys", 927 | "windows-sys 0.59.0", 928 | ] 929 | 930 | [[package]] 931 | name = "rustls-pemfile" 932 | version = "1.0.4" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 935 | dependencies = [ 936 | "base64 0.21.7", 937 | ] 938 | 939 | [[package]] 940 | name = "rustversion" 941 | version = "1.0.19" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 944 | 945 | [[package]] 946 | name = "ryu" 947 | version = "1.0.19" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 950 | 951 | [[package]] 952 | name = "schannel" 953 | version = "0.1.27" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 956 | dependencies = [ 957 | "windows-sys 0.59.0", 958 | ] 959 | 960 | [[package]] 961 | name = "security-framework" 962 | version = "2.11.1" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 965 | dependencies = [ 966 | "bitflags 2.9.0", 967 | "core-foundation", 968 | "core-foundation-sys", 969 | "libc", 970 | "security-framework-sys", 971 | ] 972 | 973 | [[package]] 974 | name = "security-framework-sys" 975 | version = "2.14.0" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 978 | dependencies = [ 979 | "core-foundation-sys", 980 | "libc", 981 | ] 982 | 983 | [[package]] 984 | name = "serde" 985 | version = "1.0.218" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 988 | dependencies = [ 989 | "serde_derive", 990 | ] 991 | 992 | [[package]] 993 | name = "serde_derive" 994 | version = "1.0.218" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 997 | dependencies = [ 998 | "proc-macro2", 999 | "quote", 1000 | "syn", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "serde_json" 1005 | version = "1.0.139" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 1008 | dependencies = [ 1009 | "itoa", 1010 | "memchr", 1011 | "ryu", 1012 | "serde", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "serde_repr" 1017 | version = "0.1.19" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 1020 | dependencies = [ 1021 | "proc-macro2", 1022 | "quote", 1023 | "syn", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "serde_urlencoded" 1028 | version = "0.7.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1031 | dependencies = [ 1032 | "form_urlencoded", 1033 | "itoa", 1034 | "ryu", 1035 | "serde", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "serde_yaml" 1040 | version = "0.9.34+deprecated" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1043 | dependencies = [ 1044 | "indexmap", 1045 | "itoa", 1046 | "ryu", 1047 | "serde", 1048 | "unsafe-libyaml", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "shlex" 1053 | version = "1.3.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1056 | 1057 | [[package]] 1058 | name = "simplelog" 1059 | version = "0.12.2" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" 1062 | dependencies = [ 1063 | "log", 1064 | "termcolor", 1065 | "time", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "slab" 1070 | version = "0.4.9" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1073 | dependencies = [ 1074 | "autocfg", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "smallvec" 1079 | version = "1.14.0" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 1082 | 1083 | [[package]] 1084 | name = "socket2" 1085 | version = "0.5.8" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1088 | dependencies = [ 1089 | "libc", 1090 | "windows-sys 0.52.0", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "stable_deref_trait" 1095 | version = "1.2.0" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1098 | 1099 | [[package]] 1100 | name = "strsim" 1101 | version = "0.8.0" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1104 | 1105 | [[package]] 1106 | name = "syn" 1107 | version = "2.0.98" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1110 | dependencies = [ 1111 | "proc-macro2", 1112 | "quote", 1113 | "unicode-ident", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "sync_wrapper" 1118 | version = "0.1.2" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1121 | 1122 | [[package]] 1123 | name = "synstructure" 1124 | version = "0.13.1" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1127 | dependencies = [ 1128 | "proc-macro2", 1129 | "quote", 1130 | "syn", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "system-configuration" 1135 | version = "0.5.1" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1138 | dependencies = [ 1139 | "bitflags 1.3.2", 1140 | "core-foundation", 1141 | "system-configuration-sys", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "system-configuration-sys" 1146 | version = "0.5.0" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1149 | dependencies = [ 1150 | "core-foundation-sys", 1151 | "libc", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "tempfile" 1156 | version = "3.17.1" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" 1159 | dependencies = [ 1160 | "cfg-if", 1161 | "fastrand", 1162 | "getrandom", 1163 | "once_cell", 1164 | "rustix", 1165 | "windows-sys 0.59.0", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "termcolor" 1170 | version = "1.4.1" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1173 | dependencies = [ 1174 | "winapi-util", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "textwrap" 1179 | version = "0.11.0" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1182 | dependencies = [ 1183 | "unicode-width", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "time" 1188 | version = "0.3.37" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 1191 | dependencies = [ 1192 | "deranged", 1193 | "itoa", 1194 | "libc", 1195 | "num-conv", 1196 | "num_threads", 1197 | "powerfmt", 1198 | "serde", 1199 | "time-core", 1200 | "time-macros", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "time-core" 1205 | version = "0.1.2" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1208 | 1209 | [[package]] 1210 | name = "time-macros" 1211 | version = "0.2.19" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 1214 | dependencies = [ 1215 | "num-conv", 1216 | "time-core", 1217 | ] 1218 | 1219 | [[package]] 1220 | name = "tinystr" 1221 | version = "0.7.6" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1224 | dependencies = [ 1225 | "displaydoc", 1226 | "zerovec", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "tokio" 1231 | version = "1.43.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1234 | dependencies = [ 1235 | "backtrace", 1236 | "bytes", 1237 | "libc", 1238 | "mio", 1239 | "pin-project-lite", 1240 | "socket2", 1241 | "windows-sys 0.52.0", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "tokio-native-tls" 1246 | version = "0.3.1" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1249 | dependencies = [ 1250 | "native-tls", 1251 | "tokio", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "tokio-util" 1256 | version = "0.7.13" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1259 | dependencies = [ 1260 | "bytes", 1261 | "futures-core", 1262 | "futures-sink", 1263 | "pin-project-lite", 1264 | "tokio", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "tower-service" 1269 | version = "0.3.3" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1272 | 1273 | [[package]] 1274 | name = "tracing" 1275 | version = "0.1.41" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1278 | dependencies = [ 1279 | "pin-project-lite", 1280 | "tracing-core", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "tracing-core" 1285 | version = "0.1.33" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1288 | dependencies = [ 1289 | "once_cell", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "try-lock" 1294 | version = "0.2.5" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1297 | 1298 | [[package]] 1299 | name = "unicode-ident" 1300 | version = "1.0.17" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 1303 | 1304 | [[package]] 1305 | name = "unicode-width" 1306 | version = "0.1.14" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1309 | 1310 | [[package]] 1311 | name = "unsafe-libyaml" 1312 | version = "0.2.11" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1315 | 1316 | [[package]] 1317 | name = "url" 1318 | version = "2.5.4" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1321 | dependencies = [ 1322 | "form_urlencoded", 1323 | "idna", 1324 | "percent-encoding", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "utf16_iter" 1329 | version = "1.0.5" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1332 | 1333 | [[package]] 1334 | name = "utf8_iter" 1335 | version = "1.0.4" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1338 | 1339 | [[package]] 1340 | name = "vault-sync" 1341 | version = "0.11.0" 1342 | dependencies = [ 1343 | "clap", 1344 | "ctrlc", 1345 | "hashicorp_vault", 1346 | "log", 1347 | "serde", 1348 | "serde_json", 1349 | "serde_repr", 1350 | "serde_yaml", 1351 | "simplelog", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "vcpkg" 1356 | version = "0.2.15" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1359 | 1360 | [[package]] 1361 | name = "vec_map" 1362 | version = "0.8.2" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1365 | 1366 | [[package]] 1367 | name = "want" 1368 | version = "0.3.1" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1371 | dependencies = [ 1372 | "try-lock", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "wasi" 1377 | version = "0.11.0+wasi-snapshot-preview1" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1380 | 1381 | [[package]] 1382 | name = "wasi" 1383 | version = "0.13.3+wasi-0.2.2" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1386 | dependencies = [ 1387 | "wit-bindgen-rt", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "wasm-bindgen" 1392 | version = "0.2.100" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1395 | dependencies = [ 1396 | "cfg-if", 1397 | "once_cell", 1398 | "rustversion", 1399 | "wasm-bindgen-macro", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "wasm-bindgen-backend" 1404 | version = "0.2.100" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1407 | dependencies = [ 1408 | "bumpalo", 1409 | "log", 1410 | "proc-macro2", 1411 | "quote", 1412 | "syn", 1413 | "wasm-bindgen-shared", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "wasm-bindgen-futures" 1418 | version = "0.4.50" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1421 | dependencies = [ 1422 | "cfg-if", 1423 | "js-sys", 1424 | "once_cell", 1425 | "wasm-bindgen", 1426 | "web-sys", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "wasm-bindgen-macro" 1431 | version = "0.2.100" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1434 | dependencies = [ 1435 | "quote", 1436 | "wasm-bindgen-macro-support", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "wasm-bindgen-macro-support" 1441 | version = "0.2.100" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1444 | dependencies = [ 1445 | "proc-macro2", 1446 | "quote", 1447 | "syn", 1448 | "wasm-bindgen-backend", 1449 | "wasm-bindgen-shared", 1450 | ] 1451 | 1452 | [[package]] 1453 | name = "wasm-bindgen-shared" 1454 | version = "0.2.100" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1457 | dependencies = [ 1458 | "unicode-ident", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "web-sys" 1463 | version = "0.3.77" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1466 | dependencies = [ 1467 | "js-sys", 1468 | "wasm-bindgen", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "winapi" 1473 | version = "0.3.9" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1476 | dependencies = [ 1477 | "winapi-i686-pc-windows-gnu", 1478 | "winapi-x86_64-pc-windows-gnu", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "winapi-i686-pc-windows-gnu" 1483 | version = "0.4.0" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1486 | 1487 | [[package]] 1488 | name = "winapi-util" 1489 | version = "0.1.9" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1492 | dependencies = [ 1493 | "windows-sys 0.59.0", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "winapi-x86_64-pc-windows-gnu" 1498 | version = "0.4.0" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1501 | 1502 | [[package]] 1503 | name = "windows-core" 1504 | version = "0.52.0" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1507 | dependencies = [ 1508 | "windows-targets 0.52.6", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "windows-link" 1513 | version = "0.1.0" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 1516 | 1517 | [[package]] 1518 | name = "windows-sys" 1519 | version = "0.48.0" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1522 | dependencies = [ 1523 | "windows-targets 0.48.5", 1524 | ] 1525 | 1526 | [[package]] 1527 | name = "windows-sys" 1528 | version = "0.52.0" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1531 | dependencies = [ 1532 | "windows-targets 0.52.6", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "windows-sys" 1537 | version = "0.59.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1540 | dependencies = [ 1541 | "windows-targets 0.52.6", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "windows-targets" 1546 | version = "0.48.5" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1549 | dependencies = [ 1550 | "windows_aarch64_gnullvm 0.48.5", 1551 | "windows_aarch64_msvc 0.48.5", 1552 | "windows_i686_gnu 0.48.5", 1553 | "windows_i686_msvc 0.48.5", 1554 | "windows_x86_64_gnu 0.48.5", 1555 | "windows_x86_64_gnullvm 0.48.5", 1556 | "windows_x86_64_msvc 0.48.5", 1557 | ] 1558 | 1559 | [[package]] 1560 | name = "windows-targets" 1561 | version = "0.52.6" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1564 | dependencies = [ 1565 | "windows_aarch64_gnullvm 0.52.6", 1566 | "windows_aarch64_msvc 0.52.6", 1567 | "windows_i686_gnu 0.52.6", 1568 | "windows_i686_gnullvm", 1569 | "windows_i686_msvc 0.52.6", 1570 | "windows_x86_64_gnu 0.52.6", 1571 | "windows_x86_64_gnullvm 0.52.6", 1572 | "windows_x86_64_msvc 0.52.6", 1573 | ] 1574 | 1575 | [[package]] 1576 | name = "windows_aarch64_gnullvm" 1577 | version = "0.48.5" 1578 | source = "registry+https://github.com/rust-lang/crates.io-index" 1579 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1580 | 1581 | [[package]] 1582 | name = "windows_aarch64_gnullvm" 1583 | version = "0.52.6" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1586 | 1587 | [[package]] 1588 | name = "windows_aarch64_msvc" 1589 | version = "0.48.5" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1592 | 1593 | [[package]] 1594 | name = "windows_aarch64_msvc" 1595 | version = "0.52.6" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1598 | 1599 | [[package]] 1600 | name = "windows_i686_gnu" 1601 | version = "0.48.5" 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" 1603 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1604 | 1605 | [[package]] 1606 | name = "windows_i686_gnu" 1607 | version = "0.52.6" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1610 | 1611 | [[package]] 1612 | name = "windows_i686_gnullvm" 1613 | version = "0.52.6" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1616 | 1617 | [[package]] 1618 | name = "windows_i686_msvc" 1619 | version = "0.48.5" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1622 | 1623 | [[package]] 1624 | name = "windows_i686_msvc" 1625 | version = "0.52.6" 1626 | source = "registry+https://github.com/rust-lang/crates.io-index" 1627 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1628 | 1629 | [[package]] 1630 | name = "windows_x86_64_gnu" 1631 | version = "0.48.5" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1634 | 1635 | [[package]] 1636 | name = "windows_x86_64_gnu" 1637 | version = "0.52.6" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1640 | 1641 | [[package]] 1642 | name = "windows_x86_64_gnullvm" 1643 | version = "0.48.5" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1646 | 1647 | [[package]] 1648 | name = "windows_x86_64_gnullvm" 1649 | version = "0.52.6" 1650 | source = "registry+https://github.com/rust-lang/crates.io-index" 1651 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1652 | 1653 | [[package]] 1654 | name = "windows_x86_64_msvc" 1655 | version = "0.48.5" 1656 | source = "registry+https://github.com/rust-lang/crates.io-index" 1657 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1658 | 1659 | [[package]] 1660 | name = "windows_x86_64_msvc" 1661 | version = "0.52.6" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1664 | 1665 | [[package]] 1666 | name = "winreg" 1667 | version = "0.50.0" 1668 | source = "registry+https://github.com/rust-lang/crates.io-index" 1669 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1670 | dependencies = [ 1671 | "cfg-if", 1672 | "windows-sys 0.48.0", 1673 | ] 1674 | 1675 | [[package]] 1676 | name = "wit-bindgen-rt" 1677 | version = "0.33.0" 1678 | source = "registry+https://github.com/rust-lang/crates.io-index" 1679 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1680 | dependencies = [ 1681 | "bitflags 2.9.0", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "write16" 1686 | version = "1.0.0" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1689 | 1690 | [[package]] 1691 | name = "writeable" 1692 | version = "0.5.5" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1695 | 1696 | [[package]] 1697 | name = "yoke" 1698 | version = "0.7.5" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1701 | dependencies = [ 1702 | "serde", 1703 | "stable_deref_trait", 1704 | "yoke-derive", 1705 | "zerofrom", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "yoke-derive" 1710 | version = "0.7.5" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1713 | dependencies = [ 1714 | "proc-macro2", 1715 | "quote", 1716 | "syn", 1717 | "synstructure", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "zerofrom" 1722 | version = "0.1.6" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1725 | dependencies = [ 1726 | "zerofrom-derive", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "zerofrom-derive" 1731 | version = "0.1.6" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1734 | dependencies = [ 1735 | "proc-macro2", 1736 | "quote", 1737 | "syn", 1738 | "synstructure", 1739 | ] 1740 | 1741 | [[package]] 1742 | name = "zerovec" 1743 | version = "0.10.4" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1746 | dependencies = [ 1747 | "yoke", 1748 | "zerofrom", 1749 | "zerovec-derive", 1750 | ] 1751 | 1752 | [[package]] 1753 | name = "zerovec-derive" 1754 | version = "0.10.3" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1757 | dependencies = [ 1758 | "proc-macro2", 1759 | "quote", 1760 | "syn", 1761 | ] 1762 | --------------------------------------------------------------------------------