From ff770532974b42f56bb8132fbf678be130d7342d Mon Sep 17 00:00:00 2001 From: Harivansh Rathi Date: Sun, 15 Mar 2026 14:09:37 -0400 Subject: [PATCH] secrets --- README.md | 7 ++ docs/secrets.md | 6 +- justfile | 3 + scripts/render-bw-shell-secrets.sh | 8 +- scripts/restore-bw-files.sh | 162 +++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 3 deletions(-) create mode 100755 scripts/restore-bw-files.sh diff --git a/README.md b/README.md index 1802110..7700ec8 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,13 @@ export BW_SESSION="$(bw unlock --raw)" just secrets-sync ``` +Restore file-based secrets from Bitwarden: + +```bash +export BW_SESSION="$(bw unlock --raw)" +just secrets-restore-files +``` + ## What Still Needs Manual Handling - Promoting vault-backed secrets into Bitwarden Secrets Manager machine-account diff --git a/docs/secrets.md b/docs/secrets.md index 5e3341a..0b0af72 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -20,8 +20,10 @@ secret values. `~/.config/secrets/shell.zsh` when present - [scripts/render-bw-shell-secrets.sh](/Users/rathi/Documents/GitHub/nix/scripts/render-bw-shell-secrets.sh) renders that file from Bitwarden vault items +- [scripts/restore-bw-files.sh](/Users/rathi/Documents/GitHub/nix/scripts/restore-bw-files.sh) + restores file-based credentials and SSH material from Bitwarden vault items - [justfile](/Users/rathi/Documents/GitHub/nix/justfile) exposes this as - `just secrets-sync` + `just secrets-sync` and `just secrets-restore-files` ## Daily Shell Flow @@ -60,7 +62,7 @@ For a fresh sandbox or new machine, the clean bootstrap is: 1. `darwin-rebuild switch` or Home Manager activation 2. authenticate `bw` 3. `just secrets-sync` -4. restore any file-based credentials you actually need from Bitwarden +4. `just secrets-restore-files` That gives you a usable dev shell quickly without committing any secret values into the repo. diff --git a/justfile b/justfile index 8f5b2b6..67d73d5 100644 --- a/justfile +++ b/justfile @@ -15,3 +15,6 @@ fmt: secrets-sync: ./scripts/render-bw-shell-secrets.sh + +secrets-restore-files: + ./scripts/restore-bw-files.sh diff --git a/scripts/render-bw-shell-secrets.sh b/scripts/render-bw-shell-secrets.sh index 230bdd1..a0dc7f5 100755 --- a/scripts/render-bw-shell-secrets.sh +++ b/scripts/render-bw-shell-secrets.sh @@ -24,7 +24,13 @@ mkdir -p "${out_dir}" read_note() { local item_name="$1" - bw get item "${item_name}" --session "${BW_SESSION}" | jq -r '.notes' + local item_id + item_id="$(bw list items --session "${BW_SESSION}" | jq -r --arg n "${item_name}" '.[] | select(.name == $n) | .id' | head -1)" + if [[ -z "${item_id}" ]]; then + echo "Bitwarden item not found: ${item_name}" >&2 + exit 1 + fi + bw get item "${item_id}" --session "${BW_SESSION}" | jq -r '.notes' } extract_env_value() { diff --git a/scripts/restore-bw-files.sh b/scripts/restore-bw-files.sh new file mode 100755 index 0000000..5dc557d --- /dev/null +++ b/scripts/restore-bw-files.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v bw >/dev/null 2>&1; then + echo "bw is not installed" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is not installed" >&2 + exit 1 +fi + +if [[ "${BW_SESSION:-}" == "" ]]; then + echo 'BW_SESSION is not set. Run: export BW_SESSION="$(bw unlock --raw)"' >&2 + exit 1 +fi + +timestamp="$(date +%Y%m%d-%H%M%S)" + +read_note() { + local item_name="$1" + local item_id + item_id="$(bw list items --session "${BW_SESSION}" | jq -r --arg n "${item_name}" '.[] | select(.name == $n) | .id' | head -1)" + if [[ -z "${item_id}" ]]; then + echo "Bitwarden item not found: ${item_name}" >&2 + exit 1 + fi + bw get item "${item_id}" --session "${BW_SESSION}" | jq -r '.notes' +} + +backup_if_exists() { + local target="$1" + if [[ -e "${target}" || -L "${target}" ]]; then + mv "${target}" "${target}.bw-bak-${timestamp}" + fi +} + +write_file() { + local target="$1" + local mode="$2" + local content="$3" + mkdir -p "$(dirname "${target}")" + backup_if_exists "${target}" + printf '%s' "${content}" > "${target}" + chmod "${mode}" "${target}" + printf 'restored %s\n' "${target}" +} + +restore_plain_note() { + local item_name="$1" + local target="$2" + local mode="$3" + write_file "${target}" "${mode}" "$(read_note "${item_name}")" +} + +restore_aws_credentials() { + local note + local access_key + local secret_key + note="$(read_note 'Machine: AWS Default Credentials')" + access_key="$(printf '%s\n' "${note}" | sed -n 's/^aws_access_key_id=//p' | head -1)" + secret_key="$(printf '%s\n' "${note}" | sed -n 's/^aws_secret_access_key=//p' | head -1)" + + write_file "${HOME}/.aws/credentials" 600 "[default] +aws_access_key_id = ${access_key} +aws_secret_access_key = ${secret_key} +" +} + +restore_gcloud_adc() { + local note + note="$(read_note 'Machine: GCloud ADC')" + + local account + local client_id + local client_secret + local quota_project_id + local refresh_token + local type + local universe_domain + + account="$(printf '%s\n' "${note}" | sed -n 's/^account=//p' | head -1)" + client_id="$(printf '%s\n' "${note}" | sed -n 's/^client_id=//p' | head -1)" + client_secret="$(printf '%s\n' "${note}" | sed -n 's/^client_secret=//p' | head -1)" + quota_project_id="$(printf '%s\n' "${note}" | sed -n 's/^quota_project_id=//p' | head -1)" + refresh_token="$(printf '%s\n' "${note}" | sed -n 's/^refresh_token=//p' | head -1)" + type="$(printf '%s\n' "${note}" | sed -n 's/^type=//p' | head -1)" + universe_domain="$(printf '%s\n' "${note}" | sed -n 's/^universe_domain=//p' | head -1)" + + local json + json="$( + jq -n \ + --arg account "${account}" \ + --arg client_id "${client_id}" \ + --arg client_secret "${client_secret}" \ + --arg quota_project_id "${quota_project_id}" \ + --arg refresh_token "${refresh_token}" \ + --arg type "${type}" \ + --arg universe_domain "${universe_domain}" \ + '{ + account: $account, + client_id: $client_id, + client_secret: $client_secret, + quota_project_id: $quota_project_id, + refresh_token: $refresh_token, + type: $type, + universe_domain: $universe_domain + }' + )" + + write_file "${HOME}/.config/gcloud/application_default_credentials.json" 600 "${json}" +} + +restore_ssh_key() { + local item_name="$1" + local rel_path="$2" + local note + local private_key + local public_key + + note="$(read_note "${item_name}")" + + private_key="$( + printf '%s\n' "${note}" | awk ' + BEGIN {section="p"; started=0} + /^path=/ {next} + started==0 && /^$/ {started=1; next} + started==1 && /^public_key:$/ {section="u"; next} + started==1 && section=="p" {print} + ' + )" + + public_key="$( + printf '%s\n' "${note}" | awk ' + BEGIN {capture=0} + /^public_key:$/ {capture=1; next} + capture==1 {print} + ' + )" + + write_file "${HOME}/.ssh/${rel_path}" 600 "${private_key}" + if [[ -n "${public_key}" ]]; then + write_file "${HOME}/.ssh/${rel_path}.pub" 644 "${public_key}" + fi +} + +restore_plain_note 'Machine: SSH Config' "${HOME}/.ssh/config" 600 +restore_plain_note 'Machine: SSH CSB Config' "${HOME}/.ssh/csb/config" 600 + +restore_ssh_key 'Machine: SSH Key atlas-ssh.txt' 'atlas-ssh.txt' +restore_ssh_key 'Machine: SSH Key csb_id_rsa_5m2zg4' 'csb/csb_id_rsa_5m2zg4' +restore_ssh_key 'Machine: SSH Key google_compute_engine' 'google_compute_engine' +restore_ssh_key 'Machine: SSH Key id_ed25519' 'id_ed25519' +restore_ssh_key 'Machine: SSH Key id_ed25519_uvacompute' 'id_ed25519_uvacompute' +restore_ssh_key 'Machine: SSH Key id_rsa_1024' 'id_rsa_1024' +restore_ssh_key 'Machine: SSH Key phinsta_ciuser' 'phinsta_ciuser' + +restore_aws_credentials +restore_gcloud_adc +restore_plain_note 'Machine: Codex Auth' "${HOME}/.codex/auth.json" 600 +restore_plain_note 'Machine: Vercel Auth' "${HOME}/Library/Application Support/com.vercel.cli/auth.json" 600