- 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.
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-idlisting 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
- 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. - SSH key:
ssh-keygen -t ed25519 -C you@psi.ch, add the.pubunder gitea → Settings → SSH/GPG Keys, verify withssh -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. - GPG key:
gpg --full-generate-key(RSA 4096, real name + PSI email, set a passphrase). - Create the store — the root store must exist first, or a named
--storewon't fully initialize (ends up with a.gpg-idbut no.git):Where the store lives locally doesn't affect syncing (git+gitea handles that) — see "Store location" below for the Bernina-specific recommendation, and just rungopass 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 mastergopass_bernina_setupinstead of doing this by hand once the repo/recipient exist.
Onboarding someone new, by hand (if not using the script above)
- Admin adds them as a gitea Collaborator (or confirms their group membership grants access).
- They register an SSH key with gitea, then:
gopass setup --remote git@gitea.psi.ch:<group>/bernina-secrets.git --alias bernina(add--name/--emailto also generate a GPG key). - Verify before going further:
gopass mountsshould showberninawith no error, and the store directory needs a.gitfolder (not just.gpg-id) — if either fails, the root store is missing (see Troubleshooting). - They export and send their public key:
gpg --armor --export you@psi.ch > pubkey.asc. - 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_setupdefault): 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 needscreate_personal_dirandunx-sf_bernina_bsgroup membership first).The catch, and the workaround:
network_drivesrejects the chmod calls thatgit init/git clone/git config/gpg --gen-key/ssh-keygenmake 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,gpgencrypt/decrypt, andgopass insert/show/syncare all confirmed to work directly on thenetwork_drivescopy. Sogopass_bernina_setupdoes the actual init/clone/keygen/config in a throwaway local scratch directory first, then copies the result ontonetwork_drivesand uses it from there — you don't need to think about this, the script handles it (see itscopy_tree()helper), but it explains why a manualgit clonestraight into~/network_drives/...will fail where the script doesn't.Use
cp -r, notrsync, for this copy —rsync -rlt(no-p/-a) works on somenetwork_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-vwith stderr combined — looks like an rsync/gvfs-fuse incompatibility specific to that share/mount).cp -rconfirmed to work in the exact same spot. The scripts were switched fromrsynctocp -raccordingly; if you're doing this by hand andrsyncmysteriously exits nonzero with no error text, trycp -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/cloneactually ran (the scratch dir) — the script fixes this withsedright after copying. If you ever hand-copy a store between locations, redo thatsedyourself orgopass mountswill fail with "password-store is not initialized" even though the files copied fine. -
/sf/bernina/config/personal/<you>/gopass-bernina(real NFS4) — whatgopass_bernina_setup(andgopass_bernina_service_setup) fall back to when~/network_drivesisn't reachable on the host you're running from. No workaround needed:git init/gpg --gen-keywork natively here. Needsunx-sf_bernina_bsgroup membership though — for thegac-berninaservice account specifically, that membership doesn't exist (create_personal_dirfails with "Permission denied"), so~/network_drivesis 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 to0700, 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/gpgcommand in a brand-new terminal/session sometimes fails (not a recipient,password-store is not initialized, or similar), and simply retrying (or runninggopass syncfirst) 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 bygopass_bernina_setup/gopass_bernina_service_setup) waits up to 5s for this before exportingGNUPGHOME/GOPASS_HOMEDIR- re-run the setup script to regenerate an olderenv.shthat 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 clonehangs forever (stuck after "Gathering information for the git repository ..." with no error, no progress, and no child process doing visible work): a confirmedgopassbug/deadlock in its post-clone bookkeeping - reproduced even against the small realbernina-secretsrepo, so it's not about repo size or a slow network.gopass_bernina_setupand the service scripts no longer usegopass clonefor this reason (they do a plaingit clone+git config core.fileMode false+gopass mounts addinstead - confirmed instant). If you ever see this while doing something by hand,Ctrl-C,gpgconf --kill gpg-agentfor any GNUPGHOME the hung command touched, and usegit clone+gopass mounts addinstead ofgopass clone.fatal: not a git repository/password-store is not initialized: the root store is missing.gopass init you@psi.chfirst, then remove and redo the broken--storeinit/clone.error: chmod on .../.git/config.lock failed: Operation not supported(or the same forgpg --gen-key/a.gpg-idwrite): you (or a script) tried togit init/clone/configorgpg --gen-keydirectly on~/network_drives. See "Store location" above — do it in a local scratch dir and copy the result over withcp -rinstead (notrsync— see below).gopass_bernina_setupalready does this; only relevant if you're doing something by hand.rsyncexits nonzero (e.g. 3) with no error text at all, even at-vwith stderr combined: confirmed on at least onenetwork_drives/SMB share (gac-bernina's) — an rsync/gvfs-fuse incompatibility, not a permissions or quota problem (cp -rto the exact same destination works fine). Usecp -r SRC/. DEST/instead; the scripts already do.sed: preserving permissions for ... Operation not supportedwhile fixing a copied gopass config: harmless —sed -istill completes the actual edit, it just can'tchmodits temp file to match on SMB. Confirm withgopass mountsafterward rather than worrying about the warning.- A background tool (e.g.
eco'sscilogintegration) falls back to its own password prompt:pinentryneeds a real terminal and didn't get one. Rungopass show bernina/...once in an actual terminal to cache the passphrase ingpg-agent. Default cache is short (600s/7200s max) — extend it in$GNUPGHOME/gpg-agent.conf:thendefault-cache-ttl 315360000 max-cache-ttl 315360000gpgconf --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 —$GNUPGHOMElives on~/network_drives(SMB), and editors that save via temp-file-then-rename (vim, nano, ...) hit the samechmod ... Operation not supportedfailure documented above forgit/gpg, which can silently fail to save the file. A single direct write has no rename step and works fine. Since$GNUPGHOMEis shared, writing this once covers every host — but each host's already-runninggpg-agentneeds its owngpgconf --reload gpg-agentto 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 berninaerrors oddly: put--storebefore the email — flags typed after the first positional argument aren't recognized. If you already hit this, check.gpg-idin 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.