From 08661e76d35f983ba0dbd90e8225e9718711df7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiz=20Poli=C4=87?= <32913827+ramizpolic@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:12:37 +0100 Subject: [PATCH] chore: update docs and makefile (#83) --- Makefile | 59 ++++++--- README.md | 323 ++------------------------------------------------ docs/API.md | 306 +++++++++++++++++++++++++++++++++++++++++++++++ docs/notes.md | 33 ------ 4 files changed, 356 insertions(+), 365 deletions(-) create mode 100644 docs/API.md delete mode 100644 docs/notes.md diff --git a/Makefile b/Makefile index d47b65a..d38b7e8 100644 --- a/Makefile +++ b/Makefile @@ -2,47 +2,72 @@ export PATH := $(abspath bin/):${PATH} -# Dependency versions -GOLANGCI_VERSION = 1.53.3 -LICENSEI_VERSION = 0.8.0 +##@ General + +# Targets commented with ## will be visible in "make help" info. +# Comments marked with ##@ will be used as categories for a group of targets. + +.PHONY: help +default: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build .PHONY: build build: ## Build binary @mkdir -p build go build -race -o build/ +##@ Checks + +.PHONY: check +check: lint test ## Run lint checks and tests + +.PHONY: test +test: ## Run tests + go test -race -v ./... + .PHONY: lint lint: lint-go lint-yaml #lint-docker lint: ## Run linters .PHONY: lint-go lint-go: - golangci-lint run $(if ${CI},--out-format github-actions,) - -#.PHONY: lint-docker -#lint-docker: -# hadolint Dockerfile + $(GOLANGCI_LINT_BIN) run $(if ${CI},--out-format github-actions,) .PHONY: lint-yaml lint-yaml: yamllint $(if ${CI},-f github,) --no-warnings . +.PHONY: license-check +license-check: ## Run license check + $(LICENSEI_BIN) check + $(LICENSEI_BIN) header + .PHONY: fmt fmt: ## Format code - golangci-lint run --fix + $(GOLANGCI_LINT_BIN) run --fix -.PHONY: test -test: ## Run tests - go test -race -v ./... - -.PHONY: license-check -license-check: ## Run license check - licensei check - licensei header +##@ Dependencies deps: bin/golangci-lint bin/licensei deps: ## Install dependencies +# Dependency versions +GOLANGCI_VERSION = 1.53.3 +LICENSEI_VERSION = 0.8.0 + +# Dependency binaries +GOLANGCI_LINT_BIN := golangci-lint +LICENSEI_BIN := licensei + +# If we have "bin" dir, use those binaries instead +ifneq ($(wildcard ./bin/.),) + GOLANGCI_LINT_BIN := bin/$(GOLANGCI_LINT_BIN) + LICENSEI_BIN := bin/$(LICENSEI_BIN) +endif + bin/golangci-lint: @mkdir -p bin curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- v${GOLANGCI_VERSION} diff --git a/README.md b/README.md index 2f71288..45aaa0d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Secret Sync [![Go Report Card](https://goreportcard.com/badge/github.com/bank-vaults/secret-sync?style=flat-square)](https://goreportcard.com/report/github.com/bank-vaults/secret-sync) -[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bank-vaults/secret-sync/ci.yaml?branch=main&style=flat-square)](https://github.com/bank-vaults/secret-sync/actions/workflows/ci.yaml?query=workflow%3ACI) ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.21-61CFDD.svg?style=flat-square) -[![go.dev - references](https://pkg.go.dev/badge/mod/github.com/bank-vaults/vault-sdk)](https://pkg.go.dev/mod/github.com/bank-vaults/vault-sdk) +[![go.dev - references](https://pkg.go.dev/badge/mod/github.com/bank-vaults/secret-sync)](https://pkg.go.dev/mod/github.com/bank-vaults/secret-sync) +
+[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bank-vaults/secret-sync/ci.yaml?branch=main&style=flat-square)](https://github.com/bank-vaults/secret-sync/actions/workflows/ci.yaml?query=workflow%3ACI) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/bank-vaults/secret-sync/badge?style=flat-square)](https://api.securityscorecards.dev/projects/github.com/bank-vaults/secret-sync) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8055/badge)](https://www.bestpractices.dev/projects/8055) **Secret Sync** exposes a generic way to interact with external secret storage systems like [HashiCorp Vault](https://www.vaultproject.io/) and provides a set of API models to interact and orchestrate the synchronization of secrets between them. @@ -34,7 +37,7 @@ This name was chosen in a rush, we are open to naming suggestions 😄 Check details about upcoming features by visiting the [project issue](https://github.com/bank-vaults/secret-sync/issues) board. -## Goal +## Goals * Provide safe and simple way to work with secrets * Common API for secret management regardless of the store backend @@ -57,309 +60,7 @@ You can find complete examples and instructions in the [EXAMPLE](EXAMPLE.md) fil ## Documentation -### Secret Store - -Secret Store defines the actual external secret storage systems that will be used for API requests. -In API requests, a secret store can be either a _source_ where the secrets are fetched from or a _target_ where -the requested secrets are synced into. - -```yaml -# Defines a specific store to use. Only one store can be specified. -secretsStore: - # Each store has a unique name and associated specs. - storeName: storeSpec -``` - -You can find all the Secret Store specifications in [pkg/apis/v1alpha1/secretstore.go](pkg/apis/v1alpha1/secretstore.go) - -
-Store Spec: HashiCorp Vault* - -#### Specs - -The following configuration selects [HashiCorp Vault](https://www.vaultproject.io/) as a secret store. -```yaml -secretsStore: - vault: - address: "" - storePath: "" - authPath: "" - role: "" - tokenPath: "" - token: "" -``` -_*Vault needs to be unsealed_. - -
- -
-Store Spec: Local Provider - -#### Specs - -Use this configuration to specify a local directory as a secret store. -Secrets are represented as unencrypted files within that directory, -where filenames define secret keys and file contents the secret values. -This store is useful for local secret consumption. -```yaml -secretsStore: - local: - storePath: "path/to/local-dir" -``` -
- -### Sync Plan - -Sync plan consists of general configurations and a list of sync actions that enable the selection, transformation, and synchronization of secrets from source to target stores. -Each sync action defines a specific mode of operation depending on its specifications. -You can use this as a reference point to create a more complete sync process based on the given requirements. - -```yaml -# Used to configure the schedule for synchronization. Optional, runs only once if empty. -# The schedule is in Cron format, see https://en.wikipedia.org/wiki/Cron -schedule: "@daily" - -# Defines sync actions, i.e. how and what will be synced. Requires at least one. -sync: - - actionSpec - - actionSpec -``` - -You can find all the Sync Plan specifications in [pkg/apis/v1alpha1/syncjob_types.go](pkg/apis/v1alpha1/syncjob_types.go) - -
-Action Spec: Synchronize a secret from reference - -#### Specs - -```yaml -sync: - # Specify which secret to fetch from source. Required. - - secretRef: - key: /path/in/source-store/key - - # Specify where the secrets will be synced to on target. Optional. - # If empty, will be the same as "secretRef.key". - target: - key: /path/in/target-store/key - - # Template defines how to transform secret before syncing to target. Optional. - # If set, either "template.rawData" or "template.data" must be specified. - # - # The template will be executed once to create a value to sync to "target.key". - # The value of the "secretRef.key" secret can be accessed via {{ .Data }}. - template: - rawData: '{{ .Data }}' # save either as a (multiline) string - data: # or as a map - secretPassword: '{{ .Data }}' -``` - -#### Example - -Synchronize a single `/tenant-1/db-username` from the source store to `/remote-db-username` on the target store. -```yaml -sync: -- secretRef: - key: /tenant-1/db-username - target: - key: /remote-db-username -``` - -
- -
-Action Spec: Synchronize multiple secrets from a query - -#### Specs - -```yaml -sync: - # Specify query for secrets to fetch from source. Required. - - secretQuery: - path: /path/in/source-store - key: - regexp: some-key-prefix-.* - - # Specify where the secrets will be synced to on target. Optional. - # > If set, every query matching secret will be synced under - # key = "{target.keyPrefix}{match.GetName()}" - # > If empty, every query matching secret will be synced under - # key = "{secretQuery.path}/{match.GetName()}". - target: - keyPrefix: /path/in/target-store/ - - # Template defines how to transform secret before syncing to target. Optional. - # If set, either "template.rawData" or "template.data" must be specified. - # - # This template will be executed for every query matching secret to create a secret - # which will be synced to "target". - # The value of (current) query secret can be accessed via {{ .Data }}. - template: - rawData: '{{ .Data }}' # save either as a (multiline) string - data: # or as a map - secretPassword: '{{ .Data }}' -``` - -#### Example - -Synchronize all secrets that match `/tenant-1/db-*` regex from the source store to `/remote-` on the target store. -```yaml -sync: -- secretQuery: - path: /tenant-1 - key: - regexp: db-.* - target: - keyPrefix: /remote- -``` - -
- -
-Action Spec: Synchronize a secret from a query - -#### Specs - -```yaml -sync: - # Specify query for secrets to fetch from source. Required. - - secretQuery: - path: /path/in/source-store - key: - regexp: some-key-prefix-.* - - # Indicate that you explicitly want to sync into a single key. Required. - flatten: true - - # Specify where the secret will be synced to on target. Required. - target: - key: /path/in/target-store/key - - # Template defines how to transform secret before syncing to target. Optional. - # If set, either "template.rawData" or "template.data" must be specified. - # - # The template will be executed once to create a value which will be synced to "target.key". - # The value for each secret from the "secretQuery" is accessible in the template - # via {{ .Data. }}, for example {{ .Data.someKeyPrefix1 }}. - template: - rawData: '{{ .Data.someKeyPrefix1 }}' # save either as a (multiline) string - data: # or as a map - secret: '{{ .Data.someKeyPrefix1 }}' -``` - -#### Example - -Fetch secrets that match `/tenant-1/db-(username|password)` regex from source store and use them -to create a new (combined) db access secret on the target store. - -```yaml -sync: -- secretQuery: - path: /tenant-1 - key: - regexp: db-(username|password) - flatten: true - target: - key: /db/access - template: - data: - type: "postgres" - username: "{{ .Data.dbUsername }}" - password: "{{ .Data.dbPassword }}" -``` - -
- - -
-Action Spec: Synchronize a secret from multiple queries and references - -#### Specs - -```yaml -sync: - # Specify (named) queries and references for secrets to fetch from source. - # At least one sync action is required. - - secretSources: - - name: action-ref - secretRef: - key: /path/in/source-store/key - - name: action-query - secretQuery: - path: /path/in/source-store - key: - regexp: some-key-prefix-.* - - # Specify where the secret will be synced to on target. Required. - target: - key: /path/in/target-store/key - - # Template defines how to transform secret before syncing to target. Optional. - # If set, either "template.rawData" or "template.data" must be specified. - # - # The template will be executed once to create a value which will be synced to "target.key". - # The value for each secret from the "secretSources" is accessible in the template via: - # > Use {{ .Data. }} for "action-ref" source. - # For example, use {{ .Data.actionRef }} - # > Use {{ .Data.. }} for "action-query" source. - # For example {{ .Data.actionQuery }} - template: - rawData: '{{ .Data.actionRef }}' - data: - secret1: '{{ .Data.actionRef }}' - secret2: '{{ .Data.actionQuery.someKeyPrefix1 }}' -``` - -#### Example - -Fetch secrets that match `/db-(1|2)/(username|password)` regex from source store and use them -to create a new (combined) db access secret on the target store. - -```yaml -sync: - - secretSources: - - name: db1 - secretRef: - path: /db-1 - key: - regexp: username|password - - name: db2 - secretRef: - path: /db-2 - key: - regexp: username|password - target: - key: /dbs-combined - template: - data: - db1_username: "{{ .Data.db1.username }}" - db1_password: "{{ .Data.db1.password }}" - db2_username: "{{ .Data.db2.username }}" - db2_password: "{{ .Data.db2.password }}" -``` - -
- -#### On Templating - -Standard golang templating is supported for sync action items. -In addition, functions such as `base64dec` and `base64enc` for decoding/encoding and -`contains`, `hasPrefix`, `hasSuffix` for string manipulation are also supported. - -### Running the synchronization - -The CLI tool provides a way to run secret synchronization between secret stores. -It requires three things: -- Path to _source store_ config file via `--source` flag -- Path to _target store_ config file via `--target` flag -- Path to _sync plan_ config file via `--plan` flag - -Note that only YAML configuration files are supported. -You can also provide optional params for CRON schedule to periodically sync secrets via `--schedule` flag. -All sync actions are indexed in logs based on their order in the sync plan config file. - -You can also use [pkg/storesync](pkg/storesync) package to run secret synchronization plan natively from Golang. -This is how the CLI works as well. +Check out the documentation at [API documentation](docs/API.md) page or on [pkg.go.dev](https://pkg.go.dev/mod/github.com/bank-vaults/secret-sync). ## Development @@ -368,6 +69,7 @@ This is how the CLI works as well. _Alternatively, install [Go](https://go.dev/dl/) on your computer then run `make deps` to install the rest of the dependencies._ Fetch required tools: + ```shell make deps ``` @@ -396,15 +98,6 @@ Some linter violations can automatically be fixed: make fmt ``` -## Useful links - -- [Contributing guide](https://bank-vaults.dev/docs/contributing/) -- [Security procedures](https://bank-vaults.dev/docs/security/) -- [Code of Conduct](https://bank-vaults.dev/docs/code-of-conduct/) -- Email: [team@bank-vaults.dev](mailto:team@bank-vaults.dev) - ## License The project is licensed under the [Apache 2.0 License](https://github.com/bank-vaults/secret-sync/blob/master/LICENSE). - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fbank-vaults%2Fsecret-sync.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fbank-vaults%2Fsecret-sync?ref=badge_large) diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..6692833 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,306 @@ +## API Documentation + +### Secret Store + +Secret Store defines the actual external secret storage systems that will be used for API requests. +In API requests, a secret store can be either a _source_ where the secrets are fetched from or a _target_ where +the requested secrets are synced into. + +```yaml +# Defines a specific store to use. Only one store can be specified. +secretsStore: + # Each store has a unique name and associated specs. + storeName: storeSpec +``` + +You can find all the Secret Store specifications in [pkg/apis/v1alpha1/secretstore.go](pkg/apis/v1alpha1/secretstore.go) + +
+Store Spec: HashiCorp Vault* + +#### Specs + +The following configuration selects [HashiCorp Vault](https://www.vaultproject.io/) as a secret store. +```yaml +secretsStore: + vault: + address: "" + storePath: "" + authPath: "" + role: "" + tokenPath: "" + token: "" +``` +_*Vault needs to be unsealed_. + +
+ +
+Store Spec: Local Provider + +#### Specs + +Use this configuration to specify a local directory as a secret store. +Secrets are represented as unencrypted files within that directory, +where filenames define secret keys and file contents the secret values. +This store is useful for local secret consumption. +```yaml +secretsStore: + local: + storePath: "path/to/local-dir" +``` +
+ +### Sync Plan + +Sync plan consists of general configurations and a list of sync actions that enable the selection, transformation, and synchronization of secrets from source to target stores. +Each sync action defines a specific mode of operation depending on its specifications. +You can use this as a reference point to create a more complete sync process based on the given requirements. + +```yaml +# Used to configure the schedule for synchronization. Optional, runs only once if empty. +# The schedule is in Cron format, see https://en.wikipedia.org/wiki/Cron +schedule: "@daily" + +# Defines sync actions, i.e. how and what will be synced. Requires at least one. +sync: + - actionSpec + - actionSpec +``` + +You can find all the Sync Plan specifications in [pkg/apis/v1alpha1/syncjob_types.go](pkg/apis/v1alpha1/syncjob_types.go) + +
+Action Spec: Synchronize a secret from reference + +#### Specs + +```yaml +sync: + # Specify which secret to fetch from source. Required. + - secretRef: + key: /path/in/source-store/key + + # Specify where the secrets will be synced to on target. Optional. + # If empty, will be the same as "secretRef.key". + target: + key: /path/in/target-store/key + + # Template defines how to transform secret before syncing to target. Optional. + # If set, either "template.rawData" or "template.data" must be specified. + # + # The template will be executed once to create a value to sync to "target.key". + # The value of the "secretRef.key" secret can be accessed via {{ .Data }}. + template: + rawData: '{{ .Data }}' # save either as a (multiline) string + data: # or as a map + secretPassword: '{{ .Data }}' +``` + +#### Example + +Synchronize a single `/tenant-1/db-username` from the source store to `/remote-db-username` on the target store. +```yaml +sync: +- secretRef: + key: /tenant-1/db-username + target: + key: /remote-db-username +``` + +
+ +
+Action Spec: Synchronize multiple secrets from a query + +#### Specs + +```yaml +sync: + # Specify query for secrets to fetch from source. Required. + - secretQuery: + path: /path/in/source-store + key: + regexp: some-key-prefix-.* + + # Specify where the secrets will be synced to on target. Optional. + # > If set, every query matching secret will be synced under + # key = "{target.keyPrefix}{match.GetName()}" + # > If empty, every query matching secret will be synced under + # key = "{secretQuery.path}/{match.GetName()}". + target: + keyPrefix: /path/in/target-store/ + + # Template defines how to transform secret before syncing to target. Optional. + # If set, either "template.rawData" or "template.data" must be specified. + # + # This template will be executed for every query matching secret to create a secret + # which will be synced to "target". + # The value of (current) query secret can be accessed via {{ .Data }}. + template: + rawData: '{{ .Data }}' # save either as a (multiline) string + data: # or as a map + secretPassword: '{{ .Data }}' +``` + +#### Example + +Synchronize all secrets that match `/tenant-1/db-*` regex from the source store to `/remote-` on the target store. +```yaml +sync: +- secretQuery: + path: /tenant-1 + key: + regexp: db-.* + target: + keyPrefix: /remote- +``` + +
+ +
+Action Spec: Synchronize a secret from a query + +#### Specs + +```yaml +sync: + # Specify query for secrets to fetch from source. Required. + - secretQuery: + path: /path/in/source-store + key: + regexp: some-key-prefix-.* + + # Indicate that you explicitly want to sync into a single key. Required. + flatten: true + + # Specify where the secret will be synced to on target. Required. + target: + key: /path/in/target-store/key + + # Template defines how to transform secret before syncing to target. Optional. + # If set, either "template.rawData" or "template.data" must be specified. + # + # The template will be executed once to create a value which will be synced to "target.key". + # The value for each secret from the "secretQuery" is accessible in the template + # via {{ .Data. }}, for example {{ .Data.someKeyPrefix1 }}. + template: + rawData: '{{ .Data.someKeyPrefix1 }}' # save either as a (multiline) string + data: # or as a map + secret: '{{ .Data.someKeyPrefix1 }}' +``` + +#### Example + +Fetch secrets that match `/tenant-1/db-(username|password)` regex from source store and use them +to create a new (combined) db access secret on the target store. + +```yaml +sync: +- secretQuery: + path: /tenant-1 + key: + regexp: db-(username|password) + flatten: true + target: + key: /db/access + template: + data: + type: "postgres" + username: "{{ .Data.dbUsername }}" + password: "{{ .Data.dbPassword }}" +``` + +
+ + +
+Action Spec: Synchronize a secret from multiple queries and references + +#### Specs + +```yaml +sync: + # Specify (named) queries and references for secrets to fetch from source. + # At least one sync action is required. + - secretSources: + - name: action-ref + secretRef: + key: /path/in/source-store/key + - name: action-query + secretQuery: + path: /path/in/source-store + key: + regexp: some-key-prefix-.* + + # Specify where the secret will be synced to on target. Required. + target: + key: /path/in/target-store/key + + # Template defines how to transform secret before syncing to target. Optional. + # If set, either "template.rawData" or "template.data" must be specified. + # + # The template will be executed once to create a value which will be synced to "target.key". + # The value for each secret from the "secretSources" is accessible in the template via: + # > Use {{ .Data. }} for "action-ref" source. + # For example, use {{ .Data.actionRef }} + # > Use {{ .Data.. }} for "action-query" source. + # For example {{ .Data.actionQuery }} + template: + rawData: '{{ .Data.actionRef }}' + data: + secret1: '{{ .Data.actionRef }}' + secret2: '{{ .Data.actionQuery.someKeyPrefix1 }}' +``` + +#### Example + +Fetch secrets that match `/db-(1|2)/(username|password)` regex from source store and use them +to create a new (combined) db access secret on the target store. + +```yaml +sync: + - secretSources: + - name: db1 + secretRef: + path: /db-1 + key: + regexp: username|password + - name: db2 + secretRef: + path: /db-2 + key: + regexp: username|password + target: + key: /dbs-combined + template: + data: + db1_username: "{{ .Data.db1.username }}" + db1_password: "{{ .Data.db1.password }}" + db2_username: "{{ .Data.db2.username }}" + db2_password: "{{ .Data.db2.password }}" +``` + +
+ +#### On Templating + +Standard golang templating is supported for sync action items. +In addition, functions such as `base64dec` and `base64enc` for decoding/encoding and +`contains`, `hasPrefix`, `hasSuffix` for string manipulation are also supported. + + +### Running the synchronization + +The CLI tool provides a way to run secret synchronization between secret stores. +It requires three things: +- Path to _source store_ config file via `--source` flag +- Path to _target store_ config file via `--target` flag +- Path to _sync plan_ config file via `--plan` flag + +Note that only YAML configuration files are supported. +You can also provide optional params for CRON schedule to periodically sync secrets via `--schedule` flag. +All sync actions are indexed in logs based on their order in the sync plan config file. + +You can also use [pkg/storesync](pkg/storesync) package to run secret synchronization plan natively from Golang. +This is how the CLI works as well. diff --git a/docs/notes.md b/docs/notes.md deleted file mode 100644 index f64b6e3..0000000 --- a/docs/notes.md +++ /dev/null @@ -1,33 +0,0 @@ -## Notes 17/08/23 -- KV stores should not necessarily be reused between bank-vaults and secret-sync -- Check how external KMS implemented their providers to get idea about how to move forward -- Mapping function should be handled by provider -- Rules API should allow options on an individual secret level (e.g. merge, transform, concat...) via e.g. templating -- Enable path for multi-destination source/dest if optimal (changes to rules API will have to change) -- Have one "global" synchronization configuration file -- Load configurations for sources/dests from e.g. Kubernetes CRs (check how external secrets k8s works to enable support for more KV stores) -- Documentation (stress the importance of early version) - -Priority: -- Ability to have rules api for secrets on individual level (can be added later) -- Add more providers (e.g. AWS, k8s) -- Secret store (actually: set store) != KV store - -## Notes 30/08/23 -- Target audience: SRE, Devs -- Query testing: makes no sense since it is difficult and cannot cover everything -- Add support for pseudo-template (e.g. bloblang) for transformations -- Add support for secret value transformations (not a priority) -- Add dry run option (does not perform API to sync to dest) -- CronJob for testing the sync in k8s (example) -- Add k8s auth support for stores (e.g. from a path to service file) -- Create validator to check if the plan is valid (without doing the API calls, but use dummy stores) -- Transform testing strategy example: have a map for dummy source as input, have a plan (dynamic), and have expected output for synced secrets -- Write a walkthrough for overall usage (as a CLI or CronJob) -- Use slog package for logging - -Priority: -- Add support for pseudo-template (e.g. bloblang) for transformations -- Create validator to check if the plan is valid (without doing the API calls, but use dummy stores) -- CronJob for testing the sync in k8s (example) -- Add dry run option (does not perform API to sync to dest)