Files
lemke_h 224c68e90b bin: consistent naming, pixi_pgroup_install, git cleanup
- 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.
2026-09-25 15:48:11 +02:00

17 KiB

Team Secrets: gopass + gitea.psi.ch

Sharing passwords/secrets across the Bernina team using gopass with a private repo on gitea.psi.ch. Replace <group>/emails below with real values. gopass/gpg are already available on Bernina hosts (/sf/bernina/bin/gopass).

How it works

  • A store is just a folder of GPG-encrypted files (bernina/laser/status-page.gpg) — no server, no database.
  • git (via gitea) only syncs and versions the ciphertext; it never sees plaintext.
  • GPG controls who can decrypt: each store has a .gpg-id listing authorized recipients. Only someone holding the matching private key can read a secret.
  • Repo access ≠ decrypt access. Being a gitea collaborator lets you pull/push ciphertext; being a GPG recipient lets you read it. Usually the same people have both, but they're separate permissions (see "Removing someone" below).

Which of the 3 roles are you, and which script do you run?

Role Who Repo access Decrypt access Script
Admin currently lemke_h gitea repo owner/admin, manages Collaborators yes — also the one who runs gopass recipients add/remove for everyone else gopass_bernina_setup
Normal group user any human on the team gitea Collaborator (Write) via group access to Bernina/bernina-secrets yes, once the admin authorizes their GPG key gopass_bernina_setup (same script, same procedure as the admin — "admin" is not a different technical setup, just the human who also manages recipients)
Service account e.g. gac-bernina none directly — no gitea account of its own; git access is a repo-scoped Deploy Key, and should be added read-only since it never pushes yes (read-only) — its GPG key is authorized as a recipient like anyone else, but it has no reason to ever insert/edit/rm gopass_bernina_service_setup — see "Service/automation accounts" below

Run <script> --help for a one-paragraph reminder of which role it's for before running it.

Quick start (new human user, incl. admin)

/sf/bernina/bin/gopass_bernina_setup

Handles the SSH key, GPG key, and cloning the store, and tells you exactly what's left (registering your SSH key with gitea, sending your GPG public key to the admin). Safe to re-run — every step checks current state first, including migrating an existing key/store from an older location instead of generating fresh ones.

Everything (GPG keyring, gopass config, and the store clone) ends up under ~/network_drives/home/gopass/ if that mount is reachable on the host you run it from, else it falls back to /sf/bernina/config/personal/<you>/ — see "Store location" below for why, and for the mechanics of how a git repo/GPG keyring can live on ~/network_drives at all despite it being SMB.

Everyday commands

Task Command
Add a secret gopass insert bernina/path/name
Add with notes (multi-line) gopass insert -m bernina/path/name
Generate a password gopass generate bernina/path/name 24
Show a secret gopass show bernina/path/name
Show password only gopass show -o bernina/path/name
Copy to clipboard gopass show -c bernina/path/name
List all gopass ls bernina
Search by name / content gopass find <term> / gopass grep <term>
Edit / delete gopass edit bernina/... / gopass rm bernina/...
Force sync gopass sync
List recipients (all stores) gopass recipients
Authorize / revoke gopass recipients add/remove --store bernina <email> (flags before the email)
Check store integrity gopass fsck

Admin: one-time setup

  1. Create a private repo on gitea.psi.ch (e.g. <group>/bernina-secrets), left empty (no README). Add teammates as Collaborators (Write access) — or, as here, via group access to a gitea Team.
  2. SSH key: ssh-keygen -t ed25519 -C you@psi.ch, add the .pub under gitea → Settings → SSH/GPG Keys, verify with ssh -T git@gitea.psi.ch. Use SSH remotes everywhere (not HTTPS) — gopass syncs via git in the background on nearly every command, and that needs to happen without prompting.
  3. GPG key: gpg --full-generate-key (RSA 4096, real name + PSI email, set a passphrase).
  4. Create the store — the root store must exist first, or a named --store won't fully initialize (ends up with a .gpg-id but no .git):
    gopass init you@psi.ch
    gopass init --store bernina you@psi.ch
    cd ~/.local/share/gopass/stores/bernina
    git remote add origin git@gitea.psi.ch:<group>/bernina-secrets.git
    git push -u origin master
    
    Where the store lives locally doesn't affect syncing (git+gitea handles that) — see "Store location" below for the Bernina-specific recommendation, and just run gopass_bernina_setup instead of doing this by hand once the repo/recipient exist.

Onboarding someone new, by hand (if not using the script above)

  1. Admin adds them as a gitea Collaborator (or confirms their group membership grants access).
  2. They register an SSH key with gitea, then: gopass setup --remote git@gitea.psi.ch:<group>/bernina-secrets.git --alias bernina (add --name/--email to also generate a GPG key).
  3. Verify before going further: gopass mounts should show bernina with no error, and the store directory needs a .git folder (not just .gpg-id) — if either fails, the root store is missing (see Troubleshooting).
  4. They export and send their public key: gpg --armor --export you@psi.ch > pubkey.asc.
  5. Admin authorizes it: gpg --import pubkey.asc && gopass recipients add --store bernina you@psi.ch — re-encrypts every existing secret for the new key automatically.

Until step 5, gopass show fails with "not a recipient" — expected, not a bug.

Removing someone / rotating secrets

gopass recipients remove --store bernina them@psi.ch

This only stops future encryption for them — git history and any clone they kept can still decrypt what they already had access to. If the removal is for cause, rotate those secrets (change the actual passwords) and remove them as a gitea collaborator too.

Restricting a subfolder (optional)

Give a subdirectory its own .gpg-id to scope it to fewer recipients:

gopass init --path ~/.local/share/gopass/stores/bernina/network admin1@psi.ch admin2@psi.ch

Other recipients see the filenames but can't decrypt them.

Store location

Doesn't affect cross-host sync (git+gitea already does that) — pick based on convenience and on which mount is actually reachable from where you run:

  • ~/network_drives (SMB, PSI's Partially Shared Home) — preferred for human accounts (gopass_bernina_setup default): the same mount, with the same content, on every PSI Linux host you log into — no separate provisioning step (unlike /sf/bernina/config/personal, which needs create_personal_dir and unx-sf_bernina_bs group membership first).

    The catch, and the workaround: network_drives rejects the chmod calls that git init/git clone/git config/gpg --gen-key/ ssh-keygen make internally while creating their lock/key files —

    error: chmod on .../.git/config.lock failed: Operation not supported
    

    — so those specific operations must never run directly there. An already-initialized repo or GPG keyring, copied on with cp -r (never with -p/-a/preserve-permissions flags — permission preservation doesn't work there either), keeps working normally afterward: git add/commit/push/pull, gpg encrypt/decrypt, and gopass insert/show/sync are all confirmed to work directly on the network_drives copy. So gopass_bernina_setup does the actual init/clone/keygen/config in a throwaway local scratch directory first, then copies the result onto network_drives and uses it from there — you don't need to think about this, the script handles it (see its copy_tree() helper), but it explains why a manual git clone straight into ~/network_drives/... will fail where the script doesn't.

    Use cp -r, not rsync, for this copy — rsync -rlt (no -p/ -a) works on some network_drives/SMB shares but confirmed to fail outright on at least one (gac-bernina's: exit code 3, "Errors selecting input/output files, dirs", with zero output even at -v with stderr combined — looks like an rsync/gvfs-fuse incompatibility specific to that share/mount). cp -r confirmed to work in the exact same spot. The scripts were switched from rsync to cp -r accordingly; if you're doing this by hand and rsync mysteriously exits nonzero with no error text, try cp -r SRC/. DEST/ instead before assuming something else is wrong.

    One more wrinkle: gopass's own config file records the store's absolute path as a string, written wherever gopass init/clone actually ran (the scratch dir) — the script fixes this with sed right after copying. If you ever hand-copy a store between locations, redo that sed yourself or gopass mounts will fail with "password-store is not initialized" even though the files copied fine.

  • /sf/bernina/config/personal/<you>/gopass-bernina (real NFS4) — what gopass_bernina_setup (and gopass_bernina_service_setup) fall back to when ~/network_drives isn't reachable on the host you're running from. No workaround needed: git init/gpg --gen-key work natively here. Needs unx-sf_bernina_bs group membership though — for the gac-bernina service account specifically, that membership doesn't exist (create_personal_dir fails with "Permission denied"), so ~/network_drives is its only working option, not just its preferred one. Don't assume this generalizes to every service account: check group membership (groups <account>) before relying on either path for a different one. Set up the personal directory first for a human/account that does have the group (locked to 0700, only you can read it):

    /sf/bernina/bin/create_personal_dir
    
  • Default (or any custom --path, no relocation at all): most portable in principle, but each host clones/generates independently — no benefit over the two options above for anything reachable across Bernina hosts.

Service/automation accounts (e.g. gac-bernina)

This is the third role — see the table above. It has no gitea account and never pushes; it only needs to read secrets. Two things a human has to set up interactively, once:

  • a gitea Deploy Key (repo-scoped SSH key, added under the repo's Settings → Deploy Keys) for git access — add it read-only (don't tick "Allow write access"), since this account should never need to push.
  • its own GPG key, authorized as a recipient the same way as any human (see "Onboarding someone new" above) — this is what actually grants it decrypt access; the deploy key only grants git access.

Run this AS the service account (become <account>, or however you get a shell as it), to make it independent of any one host's $HOME:

/sf/bernina/bin/gopass_bernina_service_setup

Same auto-detection as gopass_bernina_setup: prefers ~/network_drives/home/gopass/, falls back to /sf/bernina/config/personal/<account>/ only if that's reachable and the account has unx-sf_bernina_bs group membership. Puts the passphrase-less GPG keyring, SSH deploy key, gopass config, and store clone under whichever it picks, and writes env.sh exporting GNUPGHOME, GOPASS_HOMEDIR, and GIT_SSH_COMMAND to point there. Add one line to each host's local ~/.bashrc (only on hosts where that backend is actually reachable for this account — neither mount is guaranteed reachable from literally everywhere; e.g. confirmed missing on saresb-cons-04 for gac-bernina's ~/network_drives) — put it near the top, before any "not interactive? return" guard, so it also reaches non-interactive invocations (cron, systemd units) and not just interactive shells:

source <the ROOT_DIR printed by the script>/env.sh

Troubleshooting

  • Works fine most of the time, but the very first gopass/gpg command in a brand-new terminal/session sometimes fails (not a recipient, password-store is not initialized, or similar), and simply retrying (or running gopass sync first) makes it work: ~/network_drives (SMB, "Partially Shared Home") is mounted per login session, asynchronously - on a session that isn't already warm (e.g. you don't normally stay logged into that particular host), the mount can still be settling when your first command runs, so it silently falls back to this host's own empty local store instead. env.sh (written by gopass_bernina_setup/gopass_bernina_service_setup) waits up to 5s for this before exporting GNUPGHOME/GOPASS_HOMEDIR - re-run the setup script to regenerate an older env.sh that predates this fix. This looked at first like a host-specific bug (worked on one console, not another) but was really about how warm/cold that host's session was, not the host itself.
  • gopass clone hangs forever (stuck after "Gathering information for the git repository ..." with no error, no progress, and no child process doing visible work): a confirmed gopass bug/deadlock in its post-clone bookkeeping - reproduced even against the small real bernina-secrets repo, so it's not about repo size or a slow network. gopass_bernina_setup and the service scripts no longer use gopass clone for this reason (they do a plain git clone + git config core.fileMode false + gopass mounts add instead - confirmed instant). If you ever see this while doing something by hand, Ctrl-C, gpgconf --kill gpg-agent for any GNUPGHOME the hung command touched, and use git clone + gopass mounts add instead of gopass clone.
  • fatal: not a git repository / password-store is not initialized: the root store is missing. gopass init you@psi.ch first, then remove and redo the broken --store init/clone.
  • error: chmod on .../.git/config.lock failed: Operation not supported (or the same for gpg --gen-key/a .gpg-id write): you (or a script) tried to git init/clone/config or gpg --gen-key directly on ~/network_drives. See "Store location" above — do it in a local scratch dir and copy the result over with cp -r instead (not rsync — see below). gopass_bernina_setup already does this; only relevant if you're doing something by hand.
  • rsync exits nonzero (e.g. 3) with no error text at all, even at -v with stderr combined: confirmed on at least one network_drives/SMB share (gac-bernina's) — an rsync/gvfs-fuse incompatibility, not a permissions or quota problem (cp -r to the exact same destination works fine). Use cp -r SRC/. DEST/ instead; the scripts already do.
  • sed: preserving permissions for ... Operation not supported while fixing a copied gopass config: harmless — sed -i still completes the actual edit, it just can't chmod its temp file to match on SMB. Confirm with gopass mounts afterward rather than worrying about the warning.
  • A background tool (e.g. eco's scilog integration) falls back to its own password prompt: pinentry needs a real terminal and didn't get one. Run gopass show bernina/... once in an actual terminal to cache the passphrase in gpg-agent. Default cache is short (600s/7200s max) — extend it in $GNUPGHOME/gpg-agent.conf:
    default-cache-ttl 315360000
    max-cache-ttl 315360000
    
    then gpgconf --reload gpg-agent. It's a cache lifetime, not permanent — an agent restart (reboot/logout) clears it. Confirmed pitfall (2026-09-18): create this file with a plain shell redirect (cat > "$GNUPGHOME/gpg-agent.conf" <<EOF ... EOF), not an editor — $GNUPGHOME lives on ~/network_drives (SMB), and editors that save via temp-file-then-rename (vim, nano, ...) hit the same chmod ... Operation not supported failure documented above for git/gpg, which can silently fail to save the file. A single direct write has no rename step and works fine. Since $GNUPGHOME is shared, writing this once covers every host — but each host's already-running gpg-agent needs its own gpgconf --reload gpg-agent to pick it up (a freshly started one reads it automatically).
  • "not a recipient" / garbage output: the admin hasn't authorized your key yet, or you haven't gopass synced since they did.
  • gopass recipients add <email> --store bernina errors oddly: put --store before the email — flags typed after the first positional argument aren't recognized. If you already hit this, check .gpg-id in the store for a bogus extra line and remove it by hand.
  • gpg: decryption failed: No secret key: your private key isn't on this machine.
  • Forgot your passphrase: no recovery. Generate a new key and have an existing recipient authorize it.

Python interface

/sf/bernina/bin/gopass.py wraps the gopass CLI:

import sys; sys.path.insert(0, "/sf/bernina/bin")
from gopass import GopassStore
store = GopassStore("bernina")
store.get_password("instrument/eiger-web-ui")   # decrypts
store.list()                                     # no decryption needed

Or as a CLI: gopass.py get <name> / gopass.py list. Decrypting needs a usable GPG setup (see Troubleshooting) — calls time out cleanly (30s default) rather than hanging if none is available.