KV Engine: Recursively list keys
vfauth opened this issue ยท 47 comments
Is your feature request related to a problem? Please describe.
With a KV engine, if I want to list all keys in the directory /foo/
, it only returns keys directly under /foo/
For example, if I have the following keys:
/foo/some_key
/foo/bar/some_other_key
A LIST operation on /foo/
returns some_key
and bar/
, while I would like to have some_key
and bar/some_other_key
Describe the solution you'd like
Add a parameter to either recursively return ALL keys in the provided path.
Describe alternatives you've considered
Another way to do it would be to add a parameter specifying the depth up to which look recursively for keys.
This is a very useful feature, especially for the HTTP API
If anyone stumble upon this I made a little script (not really efficient) while we wait for a native call. Not battle tested but good enough!
./vault-list
will list everything you have access in a KV engine
./vault-list secrets/example
will list everything under secrets/example/ KV engine
#!/usr/bin/env bash
# Recursive function that will
# - List all the secrets in the given $path
# - Call itself for all path values in the given $path
function traverse {
local -r path="$1"
result=$(vault kv list -format=json $path 2>&1)
status=$?
if [ ! $status -eq 0 ];
then
if [[ $result =~ "permission denied" ]]; then
return
fi
>&2 echo "$result"
fi
for secret in $(echo "$result" | jq -r '.[]'); do
if [[ "$secret" == */ ]]; then
traverse "$path$secret"
else
echo "$path$secret"
fi
done
}
# Iterate on all kv engines or start from the path provided by the user
if [[ "$1" ]]; then
# Make sure the path always end with '/'
vaults=("${1%"/"}/")
else
vaults=$(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type =="kv") | .key')
fi
for vault in $vaults; do
traverse $vault
done
Also interested in this kind of feature.
For anyone ending up here, I created a small cli to perform recursive kv read/list operations while we wait for the native solution.
Not very tested yet, I will be fixing bugs as they show up.
@agaudreault-jive, thanks!
Btw, local readonly path="$1"
doesn't work as you might expect. use local -r path="$1"
https://stackoverflow.com/a/45409823
Issues that are not reproducible and/or have not had any interaction for a long time are stale issues. Sometimes even the valid issues remain stale lacking traction either by the maintainers or the community. In order to provide faster responses and better engagement with the community, we strive to keep the issue tracker clean and the issue count low. In this regard, our current policy is to close stale issues after 30 days. If a feature request is being closed, it means that it is not on the product roadmap. Closed issues will still be indexed and available for future viewers. If users feel that the issue is still relevant but is wrongly closed, we encourage reopening them.
Please refer to our contributing guidelines for details on issue lifecycle.
Would love this feature
I would love to have this natively, this is how I implemented it in Go for a project, it's probably not optimal or anything, but it worked for me.
var secretListPath []string
func isNil(v interface{}) bool {
return v == nil || (reflect.ValueOf(v).Kind() == reflect.Ptr && reflect.ValueOf(v).IsNil())
}
// ListSecret returns a list of secrets from Vault
func ListSecret(vaultCli *vault.Client, path string) (*vault.Secret, error) {
secret, err := vaultCli.Logical().List(path)
if err != nil {
log.Println("Couldn't list from the Vault.")
}
if isNil(secret) {
log.Printf("Couldn't list %s from the Vault.", path)
}
return secret, err
}
// RecursiveListSecret returns a list of secrets paths from Vault
func RecursiveListSecret(vaultCli *vault.Client, path string) []string {
secretList, err := ListSecret(vaultCli, path)
if err == nil && secretList != nil {
for _, secret := range secretList.Data["keys"].([]interface{}) {
if strings.HasSuffix(secret.(string), "/") {
RecursiveListSecret(vaultCli, path+secret.(string))
} else {
secretListPath = append([]string{strings.Replace(path, "metadata", "data", -1) + secret.(string)}, secretListPath...)
}
}
}
return secretListPath
}
I would love to have this natively, this is how I implemented it in Go for a project, it's probably not optimal or anything, but it worked for me.
@rnsc Just in case you didn't notice my comment above and it can help, I wrote a small go based cli for this (#5275 (comment))
Adding my vote for this feature please.
Ditto!
Essentially looking for some kind of vault-grep to find out any secret/engine/etc. that has some key, value, or pathname that matches a desired string.
One more vote for search
please implement this. It has been 4 years!
This is a must have, listing all secrets now is a major issue.
if 103 people upvoted this feature request, maybe it's about time to start adding this feature ๐ธ
For instance, Consul lists all the keys recursively, and Redis does it by default (using: KEYS *
).
And whilst the solution from @kir4h works pretty well, it would be nice to expose this feature through the API, and libraries for any language can make use of this functionality.
If anyone stumble upon this I made a little script (not really efficient) while we wait for a native call. Not battle tested but good enough!
./vault-list
will list everything you have access in a KV engine./vault-list secrets/example
will list everything under secrets/example/ KV engine#!/usr/bin/env bash # Recursive function that will # - List all the secrets in the given $path # - Call itself for all path values in the given $path function traverse { local -r path="$1" result=$(vault kv list -format=json $path 2>&1) status=$? if [ ! $status -eq 0 ]; then if [[ $result =~ "permission denied" ]]; then return fi >&2 echo "$result" fi for secret in $(echo "$result" | jq -r '.[]'); do if [[ "$secret" == */ ]]; then traverse "$path$secret" else echo "$path$secret" fi done } # Iterate on all kv engines or start from the path provided by the user if [[ "$1" ]]; then # Make sure the path always end with '/' vaults=("${1%"/"}/") else vaults=$(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type =="kv") | .key') fi for vault in $vaults; do traverse $vault done
if have not install jq command then
If anyone stumble upon this I made a little script (not really efficient) while we wait for a native call. Not battle tested but good enough!
./vault-list
will list everything you have access in a KV engine./vault-list secrets/example
will list everything under secrets/example/ KV engine#!/usr/bin/env bash # Recursive function that will # - List all the secrets in the given $path # - Call itself for all path values in the given $path function traverse { local -r path="$1" result=$(vault kv list -format=json $path 2>&1) status=$? if [ ! $status -eq 0 ]; then if [[ $result =~ "permission denied" ]]; then return fi >&2 echo "$result" fi for secret in $(echo "$result" | jq -r '.[]'); do if [[ "$secret" == */ ]]; then traverse "$path$secret" else echo "$path$secret" fi done } # Iterate on all kv engines or start from the path provided by the user if [[ "$1" ]]; then # Make sure the path always end with '/' vaults=("${1%"/"}/") else vaults=$(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type =="kv") | .key') fi for vault in $vaults; do traverse $vault doneif have not install jq command then
This script doesn't work if you have spaces in the path name. Even with passing the argument with double quotes.
I replicated that script in python
import os
import subprocess
import json
#
# Note: Before running, you must authenticate via the vault cli or the commands will fail.
#
def traverse (inputPath):
path = inputPath
result = subprocess.getoutput('vault kv list -format=json -address="'+vaultAddr+'" "'+inputPath+'"')
jResult = json.loads(result)
for secret in jResult:
if "/" in secret:
print(inputPath+secret)
traverse(inputPath+secret)
elif "/" not in jResult:
print(inputPath+secret)
parentPath = "Secret Engine/" #change to secret engine name that you are want to scan
vaultAddr = "https://vault.whatever" #change to vault address
vaults = subprocess.getoutput('vault kv list -format=json -address="'+vaultAddr+'" "'+parentPath+'"')
vault = json.loads(vaults)
for secret in vault:
print(parentPath+secret)
if "/" in secret:
traverse(parentPath+secret)
Had the same problem and ended up writing my own little tool which helps me listing KV secrets recursively in various useful formats:
Adding my vote for this feature please.
me too, seems Hashiguys doesnt want to
Hi folks. We are planning to add this feature soon, but have some questions for the people on this thread about expected behavior depending on LIST and READ permissions on secrets and paths:
- What should the behavior be if I don't have READ access to any given secret or path? Should it be included or redacted? What would you expect to see?
- What if I have LIST access to a given folder but not subfolder? Again, what would you expect to see?
- What if I have LIST or READ access to a secret within a path but not the parents within that path?
Thanks!
Good news! For your question, I would say:
- 404, to give no information about the key existence
- 404, to give no information about the key existence
- 200, because the access to this specific key has been allowed
Hi folks. We are planning to add this feature soon, but have some questions for the people on this thread about expected behavior depending on LIST and READ permissions on secrets and paths:
- What should the behavior be if I don't have READ access to any given secret or path? Should it be included or redacted? What would you expect to see?
- What if I have LIST access to a given folder but not subfolder? Again, what would you expect to see?
- What if I have LIST or READ access to a secret within a path but not the parents within that path?
Thanks!
- I agree with vfauth's thinking, give does not exist or permission denied, best to not let someone assume a secret or path exists if they don't have permission.
- Same as above
- Display the secret as the root path, or don't display any path information at all.
Hi folks. We are planning to add this feature soon, but have some questions for the people on this thread about expected behavior depending on LIST and READ permissions on secrets and paths:
What should the behavior be if I don't have READ access to any given secret or path? Should it be included or redacted? What would you expect to see?
What if I have LIST access to a given folder but not subfolder? Again, what would you expect to see?
What if I have LIST or READ access to a secret within a path but not the parents within that path?
Thanks!
For me it should silently not return anything that it cannot list or read in the sub tree.
Now of course if the given path cannot even be listed, a 404 would be good. If LIST is ok but no secrets nor any sub path can be read a 404 or a 201 empty response would be appropriate imho.
If you want to recurse from a top level path and you only have read access to a secret from two levels down I'd argue returning a 201 would be the correct behaviour. As it wouldn't show up in the UI either would it?
That's just how I would implement this for me though with the predicate that people know that recursively iterating over a path that you don't have the appropriate ACL for your token set to do is a user error and that given how this works we cannot hold the hands of the user too much without implementing insane logic.
Hi folks. We are planning to add this feature soon, but have some questions for the people on this thread about expected behavior depending on LIST and READ permissions on secrets and paths:
* What should the behavior be if I don't have READ access to any given secret or path? Should it be included or redacted? What would you expect to see? * What if I have LIST access to a given folder but not subfolder? Again, what would you expect to see? * What if I have LIST or READ access to a secret within a path but not the parents within that path? Thanks!
I think I disagree with some of the suggestions above, mainly because the topic of this issue is about recursively LIST
ing keys, not READ
ing the secret data at those keys, right?
- keys that you can
LIST
but can'tREAD
should absolutely present in the recursive list output.
a simple label/indicator for keys that you don't haveREAD
access to could be a nice enhancement, but imo the purpose of the recursive list command isn't to precisely enumerate all of your access permissions, but simply get an easy overview of the path/key structure. - for a sub-path/key where a user lacks
LIST
permissions I wouldn't expect anything under that sub-path to be present in the output, full stop.
even if the user (strangely) has other permissions on the sub-path/key likeREAD
orWRITE
, the focus here is on recursively listing keys, and ifLIST
is restricted, it's seemingly not something the admins want the user to be able to enumerate.
from a user-friendliness standpoint, I would like to see the output inform the user that the unlistable sub-path/key exists but the user lacksLIST
permission to enumerate the path further. - this is a great question, and imo the answer is an error should be promptly returned by
vault
a recursive list logically can't start at a root the user doesn't haveLIST
access to, so attempting to do so at a path whereLIST
isn't permitted should return a nice simple error informing the user they don't haveLIST
permissions at that path.
@finnstech This is great news. Is there any low/high confidence guess about when such functionality woudl be released? This quarter, this year?
Is there any progress on this issue? I'm keen on using this feature.
Also interested in this feature
Same here.
We had to super awkwardly implement width-first-search with limited depth in Terraform to be able to retrieve all the paths of secrets of a sub-tree.
very interested in this feature
Any updates on this? This would be a highly beneficial feature for our bank-vaults project.
How can you not have search yet?
How can you not have search yet?
@oyvhvi exactly!
At the moment we are implementing our own vault-cli with all workarounds due to this missing feature.
Just adding another vote for this. I know search in Vault has been a highly contentious topic for literally years at this point, but even some basic search functionality in the KV engine would be incredibly beneficial. As a mitigation for all the performance concerns that are always brought up, how about making it an option one could enable or disable on a given KV engine?
If anyone stumble upon this I made a little script (not really efficient) while we wait for a native call. Not battle tested but good enough!
./vault-list
will list everything you have access in a KV engine./vault-list secrets/example
will list everything under secrets/example/ KV engine#!/usr/bin/env bash # Recursive function that will # - List all the secrets in the given $path # - Call itself for all path values in the given $path function traverse { local -r path="$1" result=$(vault kv list -format=json $path 2>&1) status=$? if [ ! $status -eq 0 ]; then if [[ $result =~ "permission denied" ]]; then return fi >&2 echo "$result" fi for secret in $(echo "$result" | jq -r '.[]'); do if [[ "$secret" == */ ]]; then traverse "$path$secret" else echo "$path$secret" fi done } # Iterate on all kv engines or start from the path provided by the user if [[ "$1" ]]; then # Make sure the path always end with '/' vaults=("${1%"/"}/") else vaults=$(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type =="kv") | .key') fi for vault in $vaults; do traverse $vault done
I built on your code, I had a use case to export all the KV secrets for a given path and save it locally in a secrets.json
file. I refactored it to export the secrets rather than list them. My Vault was sitting behind Cloudflare Zero trust, I adjusted the script to optionally pass CF_TOKEN
to by pass Cloudflare as well, thought people might find this helpful for their use case:
#!/usr/bin/env bash
# vault-kv-export.sh
set -eo pipefail
readonly ARB_TEMP_SECRETS_FILE="arbitrary_temp_secrets.json"
readonly TEMP_SECRETS_FILE="temp_secrets.json"
readonly SECRETS_FILE="secrets.json"
log() {
local log_type="$1"
local message="$2"
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$log_type] [$timestamp] $message"
}
log_info() {
log "INFO" "$1"
}
log_error() {
log "ERROR" "$1"
exit 1
}
traverse() {
local path="$1"
local result
local headers=()
if [[ -n "${CF_TOKEN}" ]]; then
headers+=("-header" "cf-access-token=${CF_TOKEN}")
fi
result=$(vault kv list -format=json "${headers[@]}" "${path}" 2>&1) || log_error "Failed to list secrets: ${result}"
while IFS= read -r secret; do
if [[ "${secret}" == */ ]]; then
traverse "${path}${secret}"
else
local secret_data
secret_data=$(vault kv get -format=json "${headers[@]}" "${path}${secret}" | jq -r '.data') || log_error "Failed to get secret data: ${secret_data}"
if [[ "${secret_data}" != "null" ]]; then
echo "{\"path\":\"${path}${secret}\",\"value\":{\"data\":${secret_data}}}," >>"${ARB_TEMP_SECRETS_FILE}"
fi
fi
done < <(echo "${result}" | jq -r '.[]')
}
main() {
log_info "Starting secrets retrieval process."
[[ -f "${ARB_TEMP_SECRETS_FILE}" ]] && rm -f "${ARB_TEMP_SECRETS_FILE}"
[[ -f "${TEMP_SECRETS_FILE}" ]] && rm -f "${TEMP_SECRETS_FILE}"
[[ -f "${SECRETS_FILE}" ]] && rm -f "${SECRETS_FILE}"
if [[ -n "${CF_TOKEN}" ]]; then
log_info "CF_TOKEN detected."
else
log_info "CF_TOKEN not provided. Headers will not be attached to Vault requests."
fi
local vaults
if [[ "$1" ]]; then
vaults=("${1%"/"}/")
log_info "Retrieving all secrets under ${vaults[*]}.."
else
local headers=()
if [[ -n "${CF_TOKEN}" ]]; then
headers+=("-header" "cf-access-token=${CF_TOKEN}")
fi
log_info "No secret engine provided. Retrieving all secrets.."
result=$(vault secrets list -format=json "${headers[@]}" 2>&1) || log_error "Failed to list secrets engines: ${result}"
mapfile -t vaults < <(echo "${result}" | jq -r 'to_entries[] | select(.value.type=="kv") | .key')
fi
for vault in "${vaults[@]}"; do
traverse "${vault}"
done
echo "[" >"${TEMP_SECRETS_FILE}"
sed '$s/,$//' "${ARB_TEMP_SECRETS_FILE}" >>"${TEMP_SECRETS_FILE}"
echo "]" >>"${TEMP_SECRETS_FILE}"
jq . "${TEMP_SECRETS_FILE}" >"${SECRETS_FILE}"
rm "${ARB_TEMP_SECRETS_FILE}" "${TEMP_SECRETS_FILE}"
log_info "Secrets retrieval completed and saved to ${SECRETS_FILE}"
}
[[ "$0" == "${BASH_SOURCE[0]}" ]] && main "$@"
a sample output:
$ ./vault-kv-export.sh secret2/confluent_cloud
[INFO] [2023-11-03 15:06:15] Starting secrets retrieval process.
[INFO] [2023-11-03 15:06:15] CF_TOKEN detected.
[INFO] [2023-11-03 15:06:15] Retrieving all secrets under secret2/confluent_cloud/..
[INFO] [2023-11-03 15:06:27] Secrets retrieval completed and saved to secrets.json
reading secrets.json
$ cat secrets.json
[
{
"path": "secret2/confluent_cloud/global-creds",
"value": {
"data": {
"data": {
"confluent_cloud_token": "REDACTED"
},
"metadata": {
"created_time": "2023-11-02T13:14:09.616943342Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 1
}
}
}
},
{
"path": "secret2/confluent_cloud/service/credentials",
"value": {
"data": {
"data": {
"api_key": "REDACTED",
"api_secret": "REDACTED"
},
"metadata": {
"created_time": "2023-11-02T13:14:10.336792836Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 1
}
}
}
},
...
Hope it helps
I am adding my vote, is there any news about when we can have it ? or at least what are the best alternatives for the moment?
would also like to see this implemented
It would be great to get an update on this. The last official comment on this was March. Secret cycling without this is unbelievably frustrating, especially for complex systems you weren't around for at the beginning (e.g. is this value still used somewhere?).
Adding my vote - this would be super helpful for our use case!
Hi everyone,
I managed to implement a server-side recursive secret search feature by adding a new API endpoint to the KV engine.
This was done by modifying the existing KV secret plugin source code, which means that Vault needs to be recompiled locally.
You can check my work here if you're interested for carrying further testing: https://github.com/kosmos-education/vault-plugin-secrets-kv
As I mentioned in the repository, please note that this is a highly experimental feature which could potentially lead to stability issues or vulnerabilities.
In my case, the plugin was tested against a Vault instance with 6000+ secrets stored
Any progress or developments regarding this? This feature would provide significant benefits.
I know this feature request is focused on CLI/API, but are there any plans to provide a tree ui for KV mounts on top of this upcoming work? I think that would make the UI much more usable for ops that need to deal on a daily basis with a lot of static secrets.
Also interested in knowing what the community thinks about this. Cheers.
Using vault-kv-search and fzf creates a nice way to browse through kv secrets
vault-kv-search --search=path kv -r . --json | jq -r .path | uniq | fzf --preview 'vault kv get --format=yaml ${} | faq -f yaml .data
@adrianlzt What's the faq
command? (And you're missing a closing quote)
faq is like jq but with form input/output formats (yaml between them).
I'm finally using (as the first get of all keys in my environment is quite slow):
vault read -format json sys/internal/ui/mounts/ | faq -r '.data.secret | keys | .[]' | grep -vw -e sys -e cubbyhole -e identity | xargs -n 1 vaku folder list -p > $temp_vault_path_file
cat $temp_vault_path_file | fzf --preview 'vault kv get -format=yaml ${} | faq -f yaml .data.data' -q "$1 $2 $3"
6 years and no basic implementation of this feature?