Keeping Kasten Happy When VBR Rotates Its Passwords
If you protect Kubernetes with Kasten K10 and export to Veeam Backup & Replication (VBR) repositories, there's a gotcha waiting for you the day your VBR password policy kicks in.
The problem
Kasten talks to VBR using a service account. Each VBR repository you add to Kasten becomes a location profile, and each profile references a Kubernetes secret holding the credentials for that account.
Now add a scripted password rotation on the VBR side. The moment the password changes, every Kasten export to VBR starts failing, because the secret still holds the old one. And if you have several VBR repositories on the same VBR server, that means finding and updating each profile's secret individually.
Doing that by hand once is annoying. Doing it on every rotation is not a plan.
How Kasten stores it
A VBR location profile looks like this (trimmed):
kind: Profile
apiVersion: config.kio.kasten.io/v1alpha1
metadata:
name: vbr
namespace: kasten-io
spec:
type: Location
locationSpec:
type: VBR
vbr:
serverAddress: 192.168.1.153
serverPort: 9419
repoName: VBR
skipSSLVerify: true
credential:
secretType: VBRKey
secret:
apiVersion: v1
kind: secret
name: k10secret-xchdd
namespace: kasten-io
status:
validation: Success
The part that matters is spec.locationSpec.credential.secret. That points at the secret to update, and spec.locationSpec.type: VBR tells us which profiles are VBR ones.
To list the profiles and their secrets:
kubectl -n kasten-io get profiles.config.kio.kasten.io -o json |
jq -r '.items[]
| select(.spec.locationSpec.type=="VBR")
| [.metadata.name, .spec.locationSpec.credential.secret.name] | @tsv'
To see the key names inside a secret (names only, not the values):
kubectl -n kasten-io get secret k10secret-xchdd -o json | jq -r '.data | keys[]'
The script
The script does the following:
- Finds every VBR location profile in the Kasten namespace.
- Resolves the secret each one references and locates the password key.
- Shows you exactly what it is about to change.
- Asks for the new password (hidden, entered twice).
- Asks for confirmation, then patches each secret once, even if several profiles share it.
#!/usr/bin/env bash
# update-kasten-vbr-password.sh
# Finds every Kasten location profile that uses a VBR repository, locates the
# secret holding the VBR account password, and updates it with a new password
# entered at run time.
#
# Usage: ./update-kasten-vbr-password.sh [-n namespace] [-u vbr-username-filter] [-y]
# -n Kasten namespace (default: kasten-io)
# -u Only update profiles whose secret username matches this value
# -y Skip the final confirmation prompt (not recommended)
set -euo pipefail
NS="kasten-io"
USER_FILTER=""
ASSUME_YES="false"
while getopts "n:u:yh" opt; do
case "$opt" in
n) NS="$OPTARG" ;;
u) USER_FILTER="$OPTARG" ;;
y) ASSUME_YES="true" ;;
h|*) sed -n '2,11p' "$0"; exit 0 ;;
esac
done
for bin in kubectl jq base64; do
command -v "$bin" >/dev/null || { echo "ERROR: $bin is required" >&2; exit 1; }
done
echo "Scanning location profiles in namespace '$NS'..."
# Profiles whose location type is VBR, with the secret they reference
mapfile -t ROWS < <(
kubectl -n "$NS" get profiles.config.kio.kasten.io -o json |
jq -r '.items[]
| select(.spec.type=="Location" and .spec.locationSpec.type=="VBR")
| [.metadata.name,
(.spec.locationSpec.credential.secret.name // ""),
(.spec.locationSpec.credential.secret.namespace // "'"$NS"'")]
| @tsv'
)
if [[ ${#ROWS[@]} -eq 0 ]]; then
echo "No VBR location profiles found in '$NS'." >&2
exit 1
fi
# Build the work list: profile | secret ns | secret name | username | password key
declare -a PROFILES SECRET_NS SECRET_NAME PW_KEY USERNAMES
for row in "${ROWS[@]}"; do
IFS=$'\t' read -r prof sname sns <<<"$row"
if [[ -z "$sname" ]]; then
echo " SKIP $prof: no credential secret referenced"
continue
fi
if ! sjson=$(kubectl -n "$sns" get secret "$sname" -o json 2>/dev/null); then
echo " SKIP $prof: secret $sns/$sname not found"
continue
fi
# Password key: prefer exact 'password', otherwise any key containing 'pass'
key=$(jq -r '.data | keys[] ' <<<"$sjson" | grep -ix 'password' | head -n1 || true)
[[ -z "$key" ]] && key=$(jq -r '.data | keys[]' <<<"$sjson" | grep -i 'pass' | head -n1 || true)
if [[ -z "$key" ]]; then
echo " SKIP $prof: no password-like key in $sns/$sname (keys: $(jq -r '.data|keys|join(",")' <<<"$sjson"))"
continue
fi
ukey=$(jq -r '.data | keys[]' <<<"$sjson" | grep -iE '^(username|user)$' | head -n1 || true)
uname="(unknown)"
[[ -n "$ukey" ]] && uname=$(jq -r --arg k "$ukey" '.data[$k]' <<<"$sjson" | base64 -d)
if [[ -n "$USER_FILTER" && "$uname" != "$USER_FILTER" ]]; then
echo " SKIP $prof: username '$uname' does not match filter"
continue
fi
PROFILES+=("$prof"); SECRET_NS+=("$sns"); SECRET_NAME+=("$sname")
PW_KEY+=("$key"); USERNAMES+=("$uname")
done
if [[ ${#PROFILES[@]} -eq 0 ]]; then
echo "Nothing to update." >&2
exit 1
fi
echo
echo "The following secrets will be updated:"
printf ' %-35s %-35s %-20s %s\n' "PROFILE" "SECRET" "USERNAME" "KEY"
for i in "${!PROFILES[@]}"; do
printf ' %-35s %-35s %-20s %s\n' \
"${PROFILES[$i]}" "${SECRET_NS[$i]}/${SECRET_NAME[$i]}" "${USERNAMES[$i]}" "${PW_KEY[$i]}"
done
echo
# Prompt for the new password (hidden, entered twice)
read -rsp "Enter new VBR password: " NEWPW; echo
read -rsp "Confirm new VBR password: " NEWPW2; echo
if [[ -z "$NEWPW" || "$NEWPW" != "$NEWPW2" ]]; then
echo "ERROR: passwords empty or do not match." >&2
exit 1
fi
if [[ "$ASSUME_YES" != "true" ]]; then
read -rp "Update ${#PROFILES[@]} secret(s) now? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "Aborted, nothing changed."; exit 0; }
fi
ENC=$(printf '%s' "$NEWPW" | base64 -w0 2>/dev/null || printf '%s' "$NEWPW" | base64 | tr -d '\n')
unset NEWPW NEWPW2
# Several profiles can share one secret, so only patch each secret once
declare -A DONE
fail=0
for i in "${!PROFILES[@]}"; do
id="${SECRET_NS[$i]}/${SECRET_NAME[$i]}/${PW_KEY[$i]}"
if [[ -n "${DONE[$id]:-}" ]]; then
echo " OK ${PROFILES[$i]} (secret already updated)"
continue
fi
if kubectl -n "${SECRET_NS[$i]}" patch secret "${SECRET_NAME[$i]}" --type merge \
-p "{\"data\":{\"${PW_KEY[$i]}\":\"$ENC\"}}" >/dev/null; then
DONE[$id]=1
echo " OK ${PROFILES[$i]} -> ${SECRET_NS[$i]}/${SECRET_NAME[$i]}"
else
echo " FAIL ${PROFILES[$i]} -> ${SECRET_NS[$i]}/${SECRET_NAME[$i]}" >&2
fail=1
fi
done
unset ENC
echo
if [[ $fail -eq 0 ]]; then
echo "Done. Re-run a backup or check the profile status in the K10 dashboard to validate."
else
echo "Finished with errors, see above." >&2
exit 1
fi
Using it
chmod +x update-kasten-vbr-password.sh
./update-kasten-vbr-password.sh
A run looks like this:
Scanning location profiles in namespace 'kasten-io'...
The following secrets will be updated:
PROFILE SECRET USERNAME KEY
vbr kasten-io/k10secret-xchdd k10-svc password
Enter new VBR password:
Confirm new VBR password:
Update 1 secret(s) now? [y/N] y
OK vbr -> kasten-io/k10secret-xchdd
Done. Re-run a backup or check the profile status in the K10 dashboard to validate.
Useful options:
-n <namespace>if Kasten isn't inkasten-io.-u <account>to update only secrets belonging to a particular VBR account, handy if you have more than one service account in play.-yto skip the confirmation prompt. I'd leave it off unless you are driving this from automation.
Checking it worked
The profile's validation status should stay Success:
kubectl -n kasten-io get profiles.config.kio.kasten.io -o custom-columns=NAME:.metadata.name,VALIDATION:.status.validation
Then kick off an export to the VBR repository and confirm it completes.
Caveats
- The script assumes the profile layout shown above and a password key whose name is
passwordor contains "pass". If your Kasten version stores the credentials differently, it skips that profile and tells you which keys it found rather than guessing. - It writes the secret directly, so it needs RBAC permission to get and patch secrets in the Kasten namespace.
- Try it against a single non-critical repository first.
- Nothing in it touches VBR itself. If you script the rotation on the VBR side, you could chain the two: generate the new password once, set it in VBR, and feed the same value into this script.