- Commands lose their .sh extension (create_personal_dir, gopass_bernina_setup, gopass_bernina_service_setup, kerberos_ticket_renew, migrate_to_network_drive, pixi_local_sync, pixi_rebuild_local, sync_personal_config); .sh now only marks files that are sourced. kerberos_ticket_renew.sh and pixi_local_sync.sh stay as links, since they are started at login. - Sourced files gain .sh (bpy-env, anaconda_env, anaconda_bernina_env, mm_bernina); the old names stay as links. - All references in scripts, the .desktop file and the docs updated; cheatsheet.md regenerated via list_bin_commands. - New pixi_pgroup_install: installs the shared bernina pixi env into a pgroup work folder (group-shared, with its own package cache) and makes it visible in JupyterHub as bernina-<exp>-<env>. - Fix '#/bin/bash' first lines (missing '!') in 11 scripts. - Stop tracking clutter (.nfs placeholders, __pycache__, vim swap file, .bak/_bak/~ copies, delme_*, empty 'me'); add .gitignore.
392 lines
17 KiB
Bash
Executable File
392 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# gopass_bernina_setup — one-shot onboarding for a *human* PSI account
|
|
# joining the shared "bernina" gopass secrets store. See gopass.md (same
|
|
# directory) for the full picture; this automates the "Onboarding someone
|
|
# new" section.
|
|
#
|
|
# Storage backend: prefers ~/network_drives (SMB, Partially Shared Home) -
|
|
# the account's GPG keyring, gopass config, and the store clone all end up
|
|
# there, so they're already the same on every host without any separate
|
|
# provisioning step. Falls back to /sf/bernina/config/personal/<you> (NFS4)
|
|
# if network_drives isn't reachable on this host - see gopass.md
|
|
# "Store location" for the trade-off.
|
|
#
|
|
# THE WORKAROUND THIS SCRIPT USES for network_drives (confirmed by hand -
|
|
# see gopass.md): network_drives (SMB) rejects the chmod calls that
|
|
# `git init`/`git clone`/`git config`/`gpg --gen-key` make internally, so
|
|
# those must never run directly there ("Operation not supported"). An
|
|
# ALREADY-initialized repo or keyring, copied on with `cp -r` (rsync also
|
|
# works on some shares but confirmed to fail outright on at least one -
|
|
# see copy_tree() below), keeps working normally afterward:
|
|
# add/commit/push/pull, encrypt/decrypt, and `gopass insert`/`show`/`sync`
|
|
# all confirmed. So: do the actual init/clone/gen-key/config in a local
|
|
# scratch directory first, then copy the result onto network_drives and
|
|
# use it from there.
|
|
#
|
|
# What this script CANNOT do for you — gitea has no API for these, and
|
|
# they're each an action only another human/account can take:
|
|
# 1. Registering your SSH public key with gitea.psi.ch
|
|
# (Settings -> SSH/GPG Keys -> Add Key)
|
|
# 2. Getting added as a Collaborator (Write access) on the
|
|
# Bernina/bernina-secrets repo
|
|
# 3. Getting your GPG key authorized as a recipient - an existing
|
|
# recipient (currently: lemke_h) has to run `gopass recipients add`
|
|
# for you (see gopass.md) after you send them your exported
|
|
# public key
|
|
# The script detects each of these and tells you exactly what's missing
|
|
# and what to do, then stops - re-run it after fixing that one thing.
|
|
#
|
|
# Usage: gopass_bernina_setup [your-psi-email]
|
|
# Defaults to <your-username>@psi.ch.
|
|
#
|
|
# Safe to re-run: every step checks current state first and skips work
|
|
# that's already done. If you already have a working GPG key/store in an
|
|
# old location (e.g. $HOME/.gnupg, or a previous /sf/bernina-only setup),
|
|
# this OFFERS to copy it into the new location rather than generating a
|
|
# fresh one - keeps the same recipient registration, no re-authorization
|
|
# needed.
|
|
#
|
|
set -euo pipefail
|
|
|
|
GITEA_REMOTE="git@gitea.psi.ch:Bernina/bernina-secrets.git"
|
|
STORE_ALIAS="bernina"
|
|
PERSONAL_ROOT="/sf/bernina/config/personal"
|
|
BIN_DIR="/sf/bernina/bin"
|
|
|
|
log() { echo "==> $*"; }
|
|
warn() { echo "!! $*" >&2; }
|
|
ask() {
|
|
local reply
|
|
read -r -p "$1 [y/N] " reply
|
|
[[ "$reply" =~ ^[Yy]$ ]]
|
|
}
|
|
copy_tree() {
|
|
# `rsync -rlt` was the first choice here (no -p/-a - permission
|
|
# preservation doesn't work on network_drives) and works fine on some
|
|
# network_drives/SMB shares, but confirmed to fail on at least one
|
|
# (gac-bernina's: exit 3, "Errors selecting input/output files, dirs",
|
|
# with -v producing zero output even with stderr combined - looks
|
|
# like an rsync/gvfs-fuse incompatibility specific to that share).
|
|
# `cp -r` confirmed to work where that rsync failed, so use it
|
|
# everywhere instead for consistency.
|
|
mkdir -p "$2"
|
|
cp -r "$1/." "$2/"
|
|
}
|
|
|
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
cat <<'USAGE'
|
|
gopass_bernina_setup [your-psi-email]
|
|
|
|
For: any HUMAN PSI account that needs to read/write the shared "bernina"
|
|
gopass secrets store - this is the SAME script and procedure whether
|
|
you're the admin (the one who also runs `gopass recipients add` for
|
|
others) or a normal group user. There is no separate "admin mode".
|
|
|
|
NOT for the gac-bernina service account - see
|
|
gopass_bernina_service_setup for that (different: passphrase-less key,
|
|
deploy key instead of a personal SSH key, non-interactive).
|
|
|
|
Defaults to <your-username>@psi.ch. Safe to re-run.
|
|
USAGE
|
|
exit 0
|
|
fi
|
|
|
|
USERNAME="$(whoami)"
|
|
EMAIL="${1:-${USERNAME}@psi.ch}"
|
|
|
|
# --- 0. pick a storage backend ----------------------------------------------
|
|
if [[ -d "${HOME}/network_drives/home" ]]; then
|
|
BACKEND="network_drives"
|
|
ROOT_DIR="${HOME}/network_drives/home/gopass"
|
|
NEEDS_SCRATCH=1
|
|
else
|
|
BACKEND="sf_bernina"
|
|
ROOT_DIR="${PERSONAL_ROOT}/${USERNAME}"
|
|
NEEDS_SCRATCH=0
|
|
log "~/network_drives/home not reachable on this host - falling back to ${ROOT_DIR}"
|
|
fi
|
|
GNUPG_DIR="${ROOT_DIR}/gnupg"
|
|
GOPASS_HOME_DIR="${ROOT_DIR}/gopass-home"
|
|
STORE_DIR="${ROOT_DIR}/gopass-${STORE_ALIAS}"
|
|
ENV_FILE="${ROOT_DIR}/env.sh"
|
|
|
|
SCRATCH="$(mktemp -d)"
|
|
cleanup() { rm -rf "${SCRATCH}"; }
|
|
trap cleanup EXIT
|
|
|
|
echo "Setting up gopass access to the '${STORE_ALIAS}' store for ${EMAIL}"
|
|
echo "Backend: ${BACKEND} (everything will live under ${ROOT_DIR})"
|
|
echo
|
|
|
|
# --- 1. target directory -----------------------------------------------------
|
|
log "Step 1/6: target directory (${ROOT_DIR})"
|
|
if [[ "${BACKEND}" == "sf_bernina" ]]; then
|
|
"${BIN_DIR}/create_personal_dir"
|
|
fi
|
|
mkdir -p "${GOPASS_HOME_DIR}"
|
|
mkdir -m 700 -p "${GNUPG_DIR}"
|
|
|
|
# --- 2. SSH key + gitea reachability -----------------------------------------
|
|
log "Step 2/6: SSH key for gitea.psi.ch"
|
|
if [[ ! -f ~/.ssh/id_ed25519 && ! -f ~/.ssh/id_rsa ]]; then
|
|
if ask " No SSH key found. Generate one now (ssh-keygen -t ed25519)?"; then
|
|
ssh-keygen -t ed25519 -C "${EMAIL}"
|
|
else
|
|
warn " Can't continue without an SSH key - see gopass.md"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if ssh -T -o BatchMode=yes -o ConnectTimeout=8 git@gitea.psi.ch </dev/null 2>&1 | grep -qi "successfully authenticated"; then
|
|
log " SSH to gitea.psi.ch: OK"
|
|
else
|
|
warn " SSH to gitea.psi.ch did not authenticate."
|
|
warn " Add your public key (~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub) at:"
|
|
warn " https://gitea.psi.ch -> Settings -> SSH/GPG Keys -> Add Key"
|
|
warn " Then re-run this script."
|
|
exit 1
|
|
fi
|
|
|
|
# --- 3. GPG key: already in place / migrate from an old location / generate -
|
|
# NOTE: we detect "is there a usable key" by presence, not by matching
|
|
# ${EMAIL} - your real PSI email often isn't <username>@psi.ch (e.g.
|
|
# lemke_h's is henrik.lemke@psi.ch), so a literal-match check would miss
|
|
# an existing key and offer to generate a redundant, unauthorized one.
|
|
# Once a key is found/generated, EMAIL is corrected to that key's own
|
|
# uid email for every step from here on (gopass init, the final
|
|
# instructions, etc).
|
|
log "Step 3/6: GPG key"
|
|
|
|
key_email_in() {
|
|
GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null \
|
|
| awk -F: '/^uid/{print $10; exit}' | sed -n 's/.*<\(.*\)>.*/\1/p'
|
|
}
|
|
has_secret_key_in() {
|
|
GNUPGHOME="$1" gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'
|
|
}
|
|
|
|
if has_secret_key_in "${GNUPG_DIR}"; then
|
|
EMAIL="$(key_email_in "${GNUPG_DIR}")"
|
|
log " already present under ${GNUPG_DIR} (${EMAIL})"
|
|
else
|
|
OLD_GNUPGHOME=""
|
|
for candidate in "${GNUPGHOME:-}" "${HOME}/.gnupg" "${PERSONAL_ROOT}/${USERNAME}/gnupg"; do
|
|
[[ -z "${candidate}" || "${candidate}" == "${GNUPG_DIR}" ]] && continue
|
|
if has_secret_key_in "${candidate}"; then
|
|
OLD_GNUPGHOME="${candidate}"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -n "${OLD_GNUPGHOME}" ]]; then
|
|
FOUND_EMAIL="$(key_email_in "${OLD_GNUPGHOME}")"
|
|
log " found an existing key (${FOUND_EMAIL}) under ${OLD_GNUPGHOME}"
|
|
if ask " Copy it to ${GNUPG_DIR} (keeps the SAME key/recipient - no re-authorization needed)?"; then
|
|
copy_tree "${OLD_GNUPGHOME}" "${GNUPG_DIR}"
|
|
EMAIL="${FOUND_EMAIL}"
|
|
log " copied - using ${EMAIL} as your identity from here on"
|
|
else
|
|
warn " without this, decrypt calls will fail until a key exists under ${GNUPG_DIR}"
|
|
fi
|
|
else
|
|
warn " no GPG key found anywhere (checked ${GNUPG_DIR}, \$GNUPGHOME, ~/.gnupg, ${PERSONAL_ROOT}/${USERNAME}/gnupg)"
|
|
if ask " Generate one now (gpg --full-generate-key - use your REAL psi.ch email, set a real passphrase)?"; then
|
|
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
|
# gpg --gen-key does chmods internally that fail directly on
|
|
# network_drives - generate locally, then copy the result.
|
|
mkdir -m 700 -p "${SCRATCH}/gnupg"
|
|
GNUPGHOME="${SCRATCH}/gnupg" gpg --full-generate-key
|
|
copy_tree "${SCRATCH}/gnupg" "${GNUPG_DIR}"
|
|
else
|
|
GNUPGHOME="${GNUPG_DIR}" gpg --full-generate-key
|
|
# safety net in case this gpg version generated into the
|
|
# default homedir despite the GNUPGHOME override above
|
|
if ! has_secret_key_in "${GNUPG_DIR}" && has_secret_key_in "${HOME}/.gnupg"; then
|
|
copy_tree "${HOME}/.gnupg" "${GNUPG_DIR}"
|
|
fi
|
|
fi
|
|
if has_secret_key_in "${GNUPG_DIR}"; then
|
|
EMAIL="$(key_email_in "${GNUPG_DIR}")"
|
|
log " generated - using ${EMAIL} as your identity from here on"
|
|
fi
|
|
else
|
|
warn " Can't continue without a GPG key"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if ! has_secret_key_in "${GNUPG_DIR}"; then
|
|
warn " no usable key under ${GNUPG_DIR} - can't continue. Re-run and"
|
|
warn " agree to the copy/generate step above, or place a key there by hand first."
|
|
exit 1
|
|
fi
|
|
log " using GPG identity: ${EMAIL}"
|
|
export GNUPGHOME="${GNUPG_DIR}"
|
|
|
|
# --- 4. root store -------------------------------------------------------------
|
|
log "Step 4/6: gopass root store"
|
|
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
|
if gopass mounts >/dev/null 2>&1; then
|
|
log " already initialized"
|
|
elif [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
|
GNUPGHOME="${GNUPG_DIR}" GOPASS_HOMEDIR="${SCRATCH}/gopass-home" gopass init "${EMAIL}"
|
|
(cd "${SCRATCH}/gopass-home/.local/share/gopass/stores/root" && git config core.fileMode false)
|
|
copy_tree "${SCRATCH}/gopass-home" "${GOPASS_HOME_DIR}"
|
|
sed -i "s|${SCRATCH}/gopass-home|${GOPASS_HOME_DIR}|g" "${GOPASS_HOME_DIR}/.config/gopass/config"
|
|
else
|
|
gopass init "${EMAIL}"
|
|
fi
|
|
|
|
# --- 5. clone/mount the bernina store ----------------------------------------
|
|
log "Step 5/6: '${STORE_ALIAS}' store at ${STORE_DIR}"
|
|
if gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
|
log " already mounted"
|
|
elif [[ -d "${STORE_DIR}/.git" ]]; then
|
|
log " directory already looks cloned, mounting it"
|
|
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
|
elif [[ -d "${STORE_DIR}" ]]; then
|
|
warn " ${STORE_DIR} exists but has no .git - a previous attempt likely"
|
|
warn " failed partway. See gopass.md Troubleshooting before removing"
|
|
warn " it by hand (confirm it really lacks .git first)."
|
|
exit 1
|
|
else
|
|
# Plain `git clone` + `gopass mounts add`, deliberately NOT `gopass
|
|
# clone`: confirmed by hand that `gopass clone` can hang indefinitely
|
|
# (Go-level deadlock, wchan futex_do_wait, zero subprocesses/network
|
|
# activity - not a slow/big-repo thing, reproduced even against the
|
|
# tiny real bernina-secrets repo) at its post-clone "Gathering
|
|
# information for the git repository" bookkeeping step. A plain git
|
|
# clone plus `gopass mounts add` (the same call already used just
|
|
# above when a store directory is pre-existing) reaches the same end
|
|
# state without going through that codepath - confirmed instant
|
|
# against the real repo.
|
|
CLONE_DEST="${STORE_DIR}"
|
|
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
|
CLONE_DEST="${SCRATCH}/${STORE_ALIAS}"
|
|
fi
|
|
CLONE_ERR="$(mktemp)"
|
|
if ! git clone "${GITEA_REMOTE}" "${CLONE_DEST}" 2>"${CLONE_ERR}"; then
|
|
cat "${CLONE_ERR}" >&2
|
|
if grep -qi "not authorized\|access rights\|could not read from remote\|permission denied" "${CLONE_ERR}"; then
|
|
warn " Repo access not granted yet. Ask the admin (lemke_h) to add you"
|
|
warn " as a Collaborator (Write access) on Bernina/bernina-secrets:"
|
|
warn " https://gitea.psi.ch/Bernina/bernina-secrets/settings/collaboration"
|
|
warn " Then re-run this script."
|
|
fi
|
|
rm -f "${CLONE_ERR}"
|
|
exit 1
|
|
fi
|
|
rm -f "${CLONE_ERR}"
|
|
(cd "${CLONE_DEST}" && git config core.fileMode false)
|
|
if [[ "${NEEDS_SCRATCH}" -eq 1 ]]; then
|
|
copy_tree "${CLONE_DEST}" "${STORE_DIR}"
|
|
fi
|
|
gopass mounts add "${STORE_ALIAS}" "${STORE_DIR}"
|
|
fi
|
|
|
|
# verify (don't skip this, a broken mount fails confusingly later)
|
|
if ! gopass mounts 2>/dev/null | grep -q "${STORE_ALIAS} ("; then
|
|
warn " mount verification failed - see gopass.md Troubleshooting"
|
|
exit 1
|
|
fi
|
|
if [[ ! -d "${STORE_DIR}/.git" ]]; then
|
|
warn " ${STORE_DIR} has no .git - clone did not fully succeed"
|
|
exit 1
|
|
fi
|
|
log " mounted and verified OK"
|
|
|
|
# --- 6. env.sh + export pubkey + tell them what's left -----------------------
|
|
log "Step 6/6: writing ${ENV_FILE} and exporting your GPG public key"
|
|
if [[ "${BACKEND}" == "network_drives" ]]; then
|
|
cat > "${ENV_FILE}" <<EOF
|
|
# Source this from ~/.bashrc (LOCAL to each host) to make your gopass
|
|
# access to '${STORE_ALIAS}' work the same on every host where ${ROOT_DIR}
|
|
# is reachable. Generated by gopass_bernina_setup - see gopass.md.
|
|
#
|
|
# ~/network_drives (SMB, "Partially Shared Home") is mounted per login
|
|
# session, asynchronously - on a freshly opened terminal/session it can
|
|
# take a moment to appear. Without this wait, the first gopass command of
|
|
# a new session can race the automount and silently fall back to an
|
|
# empty LOCAL store (this host's own \$HOME, not the shared one) - looks
|
|
# like "not a recipient" or "password-store is not initialized" for no
|
|
# obvious reason, and typically "fixes itself" on retry a moment later.
|
|
# Wait briefly (up to 5s) here instead, once per shell, so the first real
|
|
# command doesn't lose that race. See gopass.md Troubleshooting.
|
|
__gopass_wait_n=0
|
|
while [ ! -d "${GNUPG_DIR}" ] && [ "\${__gopass_wait_n}" -lt 20 ]; do
|
|
sleep 0.25
|
|
__gopass_wait_n=\$((__gopass_wait_n + 1))
|
|
done
|
|
unset __gopass_wait_n
|
|
|
|
export GNUPGHOME="${GNUPG_DIR}"
|
|
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
|
EOF
|
|
else
|
|
cat > "${ENV_FILE}" <<EOF
|
|
# Source this from ~/.bashrc (LOCAL to each host) to make your gopass
|
|
# access to '${STORE_ALIAS}' work the same on every host where ${ROOT_DIR}
|
|
# is reachable. Generated by gopass_bernina_setup - see gopass.md.
|
|
export GNUPGHOME="${GNUPG_DIR}"
|
|
export GOPASS_HOMEDIR="${GOPASS_HOME_DIR}"
|
|
EOF
|
|
fi
|
|
|
|
PUBKEY_FILE="${ROOT_DIR}/${USERNAME}-gopass-pubkey.asc"
|
|
gpg --armor --export "${EMAIL}" > "${PUBKEY_FILE}"
|
|
log " wrote ${PUBKEY_FILE}"
|
|
|
|
# is this identity already an authorized recipient? (true for anyone
|
|
# re-running this script after migrating an already-authorized key to a
|
|
# new location - e.g. the admin's own re-run - not just for brand-new
|
|
# people)
|
|
ALREADY_RECIPIENT=0
|
|
if gopass recipients 2>/dev/null | grep -qi "<${EMAIL}>"; then
|
|
ALREADY_RECIPIENT=1
|
|
fi
|
|
|
|
echo
|
|
echo "======================================================================"
|
|
# ~/.bashrc itself may be force-copied (not symlinked) from this file on
|
|
# every login, with no diff check, per PSI's Partially Shared Home
|
|
# 'copy:' mechanism (~/network_drives/home/linux_home_links.yaml) -
|
|
# confirmed 2026-09-18. If so, an edit to ~/.bashrc directly would be
|
|
# silently lost at the next login, so point at the real source instead.
|
|
BASHRC_TEMPLATE="${HOME}/network_drives/home/linux_config/bashrc"
|
|
if [[ -f "${BASHRC_TEMPLATE}" ]]; then
|
|
echo "Add ONE line to ${BASHRC_TEMPLATE} - NOT to ~/.bashrc"
|
|
echo "directly: your ~/.bashrc here gets force-copied from that file on"
|
|
echo "every login with no diff check (see the 'copy:' section of"
|
|
echo "~/network_drives/home/linux_home_links.yaml), so a direct edit to"
|
|
echo "~/.bashrc would silently vanish at your next login."
|
|
else
|
|
echo "Add ONE line to this host's LOCAL ~/.bashrc (not shared - do this once"
|
|
echo "per host, or replace any older GNUPGHOME/GOPASS_HOMEDIR exports there)."
|
|
fi
|
|
echo "Put it NEAR THE TOP - before any '[are we interactive?] || return'"
|
|
echo "guard - so it also reaches non-interactive shells (VS Code Remote-SSH,"
|
|
echo "cron, some Jupyter kernels), which would otherwise silently fall back"
|
|
echo "to this host's own empty local gopass store instead of erroring:"
|
|
echo
|
|
echo " source ${ENV_FILE}"
|
|
echo
|
|
if [[ "${ALREADY_RECIPIENT}" -eq 1 ]]; then
|
|
echo "You're already an authorized recipient of '${STORE_ALIAS}' (${EMAIL}) -"
|
|
echo "nothing else to do. Open a fresh shell (so the line above takes"
|
|
echo "effect), then 'gopass show ${STORE_ALIAS}/...' should work (it'll"
|
|
echo "ask for your GPG passphrase once, then cache it)."
|
|
else
|
|
echo "One step only an existing recipient can do for you:"
|
|
echo
|
|
echo " Send ${PUBKEY_FILE} to the store admin (lemke_h) - internal email,"
|
|
echo " Mattermost/Slack DM, or in person. They will run:"
|
|
echo
|
|
echo " gpg --import ${USERNAME}-gopass-pubkey.asc"
|
|
echo " gopass recipients add --store ${STORE_ALIAS} ${EMAIL}"
|
|
echo
|
|
echo "Until then, 'gopass show ${STORE_ALIAS}/...' will fail with a"
|
|
echo "'not a recipient' error - that's expected, not a bug (see gopass.md)."
|
|
fi
|
|
echo "======================================================================"
|