Every Raspberry Pi that needs to git clone or git pull from a private
GTMichelli-Dev repo (pi-network-setup, camera-capture-service,
qb-sync-service) authenticates through one GitHub App installed on the
org. No personal access tokens, no SSH keys, no per-Pi GitHub accounts.
web-print-service, scale-reader-service and this repo are public and clone
with no credentials at all — worth knowing, because it makes them useless for
testing whether any of this works, and because it is exactly why the bootstrap
below can run on a Pi that cannot yet authenticate to anything. foundation
and pi-network-setup are private, so either one is a real test.
This repo is the single source of truth for the scripts and this document. It lives on its own so every service repo can link to one copy instead of keeping its own. The service repos — foundation, web-print-service, scale-reader-service — point here from their release notes.
Two commands on the Pi — a fresh Pi OS Lite box with no git, or Raspberry Pi
Connect's web shell where pasting is all you can do:
curl -fsSL -o /tmp/pga-install.sh https://github.com/GTMichelli-Dev/pi-git-auth/releases/latest/download/install.sh
bash /tmp/pga-install.shIt offers the Installation ID as the default (press Enter), takes the PEM as a paste — BEGIN line to END line, with nothing to type to end it — and does the rest: fetches the helpers from the release, writes the conf, registers the credential helper, and smoke-tests against a private repo.
Nothing to prepare on the Pi first: no PEM file copied over, no git, and no
</dev/tty on the command line — the prompts read the terminal directly, so a
bracketed paste can't run away with them. Re-run it any time to refresh the
helpers or rotate the key.
For a scripted rollout, skip both prompts:
sudo bash /tmp/pga-install.sh --install-id 145563826 --pem /tmp/michelli-app.pemEverything else on this page is the long way round, for when that does not fit or when you want to watch each part happen.
| Field | Value |
|---|---|
| Name | michelli-fleet |
| Owned by | @GTMichelli-Dev (org-owned) |
| App ID | 4260960 |
| Client ID | Iv23livFZQOXhMbgSTed (preferred JWT issuer per GitHub) |
| Installation ID | 145563826 |
| Permissions | Contents: Read-only |
| PEM | downloaded once when the App was created (kept off-repo) |
The App has a private key (.pem file). On each Pi:
- The
.pemlives at/etc/michelli/github-app.pem(mode0644— see "Why so loose" below). - A token-mint helper (
/usr/local/bin/michelli-github-app-token) signs a short-lived JWT with the PEM (using the Client ID as theissclaim), swaps it for a 1-hour installation access token, and caches the token to/tmp/michelli-gh-token-$UID. - A git credential helper (
/usr/local/bin/git-credential-michelli) is registered forhttps://github.com/GTMichelli-Dev/*in/etc/gitconfig. Git calls it before every operation that needs auth; it returnsusername=x-access-tokenplus the freshly-minted token.
End result: plain git clone https://github.com/GTMichelli-Dev/<anything>.git
and git pull work silently from any user on the Pi. No PAT, no env vars, no
SSH config, no group memberships to chase.
The same token works for release assets, which git never sees — so the install
also drops a fetch-release command (/usr/local/bin/fetch-release, with
fetch_release beside it as a symlink):
fetch-release foundation-web-linux-x64.tar.gz # from GTMichelli-Dev/foundation
fetch-release --repo camera-capture-service linux-arm64.tar.gz
fetch-release --version 1.36.0 foundation-web-linux-x64.tar.gz
fetch-release --list # what the latest release holdsIt defaults to GTMichelli-Dev/foundation, accepts a full asset name or any
ending unique within the release, and prints the release's asset list when the
name matches nothing. Both spellings are installed because the service repos'
release notes say fetch_release <asset> — that was a shell function you pasted
into every session, and those instructions now work verbatim with nothing
pasted.
Token order: the App minter, then git's credential helper (asked with the repo
path — see Troubleshooting), then GH_TOKEN/GITHUB_TOKEN,
then it asks. So it also works from a workstation:
GH_TOKEN=$(gh auth token) fetch-release ....
It needs curl and jq, both of which the installer puts on the box.
/etc/michelli/github-app.pem is mode 0644 — readable by any local user on
the box. On a single-user scale-house or kiosk Pi the security benefit of
tightening it is nil: anyone with shell access already has sudo, which can
cat the PEM regardless of group. Group-restricted perms also interact badly
with Pi Connect's web shell, where sessions are sticky and don't reliably pick
up new group membership without a full browser-tab restart — real time lost
per Pi, for nothing.
On a multi-tenant Linux host this would be a downgrade and the group mechanism would be worth having back. The Michelli fleet doesn't have any of those.
- The PEM — the
.pemfile downloaded when the App was created. Keep it on your laptop or in a password manager, and paste its contents into the Pi during the bootstrap below. It is never committed to a repo: this repo is public, so a PEM committed here would be published to the world. - The Installation ID —
145563826for the current GTMichelli-Dev installation (visible in the URLhttps://github.com/organizations/GTMichelli-Dev/settings/installations/145563826). If you install the App on additional repos later, the same Installation ID covers them, as long as the new repos are checked into the existing installation.
The App ID and Client ID are already baked into
scripts/setup-pi-github-app.sh — you
don't pass them.
The Pi Connect web shell mangles multi-line bracketed pastes (the ^[[201~
end marker gets appended to the last line), so this path is structured as
single-line commands plus a nano step for the multi-line PEM. Run each step
in order. Each one is a single line, safe to copy-paste end to end.
For most Pis, Quick start above does all of this in one script; use these steps when you want to see each part happen, or when the paste-driven script has failed and you are working out why.
Step 1 — install deps, create the config dir.
sudo apt-get update -y && sudo apt-get install -y git curl jq openssl && sudo install -d -m 0755 /etc/michelliStep 2 — get the helper scripts. This repo is public, so the Pi can clone
it before it can authenticate to anything — no nano-pasting of helper scripts
needed. Sparse checkout keeps it to the scripts/ folder:
git clone --filter=blob:none --sparse https://github.com/GTMichelli-Dev/pi-git-auth.git ~/pi-git-auth && git -C ~/pi-git-auth sparse-checkout set scriptsStep 3 — paste the PEM. Open nano and paste the whole
-----BEGIN ... -----END ... block, every line. Save with Ctrl+O + Enter,
exit with Ctrl+X:
sudo nano /etc/michelli/github-app.pemThen set its mode:
sudo chmod 0644 /etc/michelli/github-app.pemStep 4 — run the installer. It writes the conf, installs both helpers,
registers the credential helper in /etc/gitconfig, and smoke-tests with a
real token mint. The PEM is already in place from step 3, so no --pem here:
sudo bash ~/pi-git-auth/scripts/setup-pi-github-app.sh --install-id 145563826Step 5 — smoke test:
git ls-remote https://github.com/GTMichelli-Dev/pi-network-setup.git HEADShould print a SHA and HEAD with no prompt. Test against a private repo
— this repo, web-print-service and scale-reader-service are public and
answer without the credential helper being involved at all, so they would
succeed even on a Pi where this all failed. If it prompts for a username, see
Troubleshooting.
You can now git clone any GTMichelli-Dev repo on this Pi.
Every release
carries install.sh on its own, the scripts as a tarball, and each script
individually. This is the path when the Pi has curl but not git, or when
you want a pinned version rather than whatever main says today.
Quick start is this path with the prompts doing the work.
install.sh pins with --version, so a fleet can be rolled out against one
known release:
curl -fsSL -o /tmp/pga-install.sh https://github.com/GTMichelli-Dev/pi-git-auth/releases/download/v1.1.0/install.sh
bash /tmp/pga-install.sh --version 1.1.0Driving setup-pi-github-app.sh yourself, with the PEM already on the box:
curl -fsSL -o pga.tar.gz https://github.com/GTMichelli-Dev/pi-git-auth/releases/latest/download/pi-git-auth.tar.gz
mkdir -p /tmp/pga && tar -xzf pga.tar.gz -C /tmp/pga
sudo bash /tmp/pga/setup-pi-github-app.sh --install-id 145563826 --pem /path/to/michelli-app.pemOn that last path take the tarball, not a single script.
setup-pi-github-app.sh installs michelli-github-app-token.sh and
git-credential-michelli.sh from alongside itself and stops with an error if
they are not there, so a lone setup-pi-github-app.sh cannot do the job.
install.sh is the exception — downloaded on its own it fetches the tarball
for you, which is why the quick start is a single asset. The other scripts are
published for reading and for replacing one helper on a Pi that is already set
up.
When you have shell and scp access from a workstation that has this repo and the PEM already on disk:
PI=admin@pi-hostname.local
PEM=/path/to/michelli-app.pem
INSTALL_ID=145563826
scp scripts/setup-pi-github-app.sh \
scripts/michelli-github-app-token.sh \
scripts/git-credential-michelli.sh \
"$PEM" "$PI:/tmp/"
ssh "$PI" "sudo bash /tmp/setup-pi-github-app.sh \
--install-id $INSTALL_ID \
--pem /tmp/$(basename "$PEM")"
ssh "$PI" "shred -u /tmp/$(basename "$PEM") && rm -f /tmp/setup-pi-github-app.sh /tmp/michelli-github-app-token.sh /tmp/git-credential-michelli.sh"Same end state as path A, and no nano step — scp doesn't mangle the PEM the way a browser terminal does.
Once this repo is on the Pi:
sudo bash ~/pi-git-auth/scripts/setup-pi-github-app.sh # refresh helpers from repo
sudo bash ~/pi-git-auth/scripts/setup-pi-github-app.sh --pem /tmp/new.pem # rotate PEM
sudo bash ~/pi-git-auth/scripts/setup-pi-github-app.sh --install-id <N> # re-install or repoint installationEach run is idempotent. Re-running with no flags refreshes the helper scripts
(scripts/michelli-github-app-token.sh / scripts/git-credential-michelli.sh)
from the current checkout — useful after a git pull that updates them.
- Token lifetime is 1 hour. The cache file
/tmp/michelli-gh-token-$UIDholds the token until 60s before expiry, thenmichelli-github-app-tokenmints a new one transparently. - Pis need internet to mint tokens. A fully offline Pi can't clone or pull regardless of auth model — same as PATs and deploy keys.
- PEM rotation. GitHub Apps let you generate a new private key without
invalidating the old. Generate a new PEM in the App settings, distribute it
to the fleet (
--pemflag), then revoke the old one. No Installation ID change needed. - Off-boarding a stolen Pi. If you can't recover it, generate a new App PEM, revoke the old, and re-bootstrap the fleet. Any cached token on the lost Pi keeps working for up to its remaining ~1h validity, but no new ones can be minted.
- Audit. Every git operation through the App shows up in the org's audit log under the App's identity — cleaner than a PAT model where every clone looks like a user action.
Already done for GTMichelli-Dev; recorded here for rebuilds. Org Settings
→ Developer settings → GitHub Apps → New GitHub App, with
Contents: Read-only and nothing else, Webhook → Active unchecked, and
installable only on this account. Create it, note the App ID and Client
ID, Generate a private key (that is the .pem — the only secret in this
scheme), then Install App on the org across all repositories. The number
ending the resulting URL is the Installation ID.
Bake the App ID and Client ID into the CLIENT_ID_DEFAULT and
APP_ID_DEFAULT lines near the top of scripts/setup-pi-github-app.sh and
commit, so per-Pi bootstraps only need the Installation ID and the PEM.
git ls-remote prompts for a username — the credential helper isn't
being called, or is failing silently. Run the minter directly to see the
error:
michelli-github-app-tokenCommon failures:
cannot read /etc/michelli/github-app.conf— the conf isn't readable. Checkls -l /etc/michelli/; it should be0644.cannot read /etc/michelli/github-app.pem— same fix, mode0644.token mint failed: {"message": "Bad credentials" ...}— the PEM doesn't match the App ID / Client ID. ConfirmCLIENT_IDin/etc/michelli/github-app.confmatches the App's settings page. A PEM pasted through a browser terminal can also arrive corrupted — verify it withsudo openssl rsa -in /etc/michelli/github-app.pem -noout -check.token mint failed: {"message": "Not Found" ...}— the Installation ID is wrong, or the App isn't installed on the repo. Checkhttps://github.com/organizations/GTMichelli-Dev/settings/installations/145563826.
Cannot find helper scripts alongside ... — you downloaded
setup-pi-github-app.sh on its own. It needs the two helpers beside it; take
install.sh, which fetches them itself, or pi-git-auth.tar.gz from the
release. See
Bootstrap path B.
Could not download ...pi-git-auth.tar.gz — install.sh was run on its
own and could not reach GitHub. Check the Pi's internet connection, and check
--version names a release that exists (latest is the default and always
does).
ls-remote succeeds but proves nothing — check you didn't test against
pi-git-auth, web-print-service or scale-reader-service. They are public
and answer without any credential helper involved. foundation and
pi-network-setup are private and do prove something.
/etc/gitconfig permission denied — the system gitconfig was created
with too-tight perms by an earlier bootstrap. Fix:
sudo chmod 0644 /etc/gitconfig| Path | Mode | Purpose |
|---|---|---|
/etc/michelli/ |
0755 | Config directory |
/etc/michelli/github-app.pem |
0644 | App private key (single-user-Pi tradeoff — see "Why so loose") |
/etc/michelli/github-app.conf |
0644 | CLIENT_ID= / APP_ID= / INSTALL_ID= (public IDs only, not secret) |
/usr/local/bin/michelli-github-app-token |
0755 | Token minter — signs the JWT, exchanges it for an installation token, caches it |
/usr/local/bin/git-credential-michelli |
0755 | Git credential helper — calls the minter, formats output for git |
/usr/local/bin/fetch-release |
0755 | Downloads a release asset by name (git credentials do not cover these) |
/usr/local/bin/fetch_release |
symlink | The spelling the service repos' release notes use |
/etc/gitconfig |
0644 | Registers the helper for https://github.com/GTMichelli-Dev/* (system-wide) |
/tmp/michelli-gh-token-$UID |
0600 | Per-user token cache (regenerated as needed; ephemeral) |
| Path | Purpose |
|---|---|
scripts/install.sh |
The one-shot front door. Prompts for the Installation ID and PEM, fetches the rest from the release if it is alone, hands off to the installer. |
scripts/setup-pi-github-app.sh |
The installer. Needs the two helpers beside it. |
scripts/michelli-github-app-token.sh |
Token minter, installed to /usr/local/bin. |
scripts/git-credential-michelli.sh |
Git credential helper, installed to /usr/local/bin. |
scripts/fetch-release.sh |
Release-asset downloader, installed to /usr/local/bin/fetch-release. |
scripts/pi-connect-github-auth.sh |
The older paste wrapper for the Pi Connect web shell: apt-installs git, clones this repo, then prompts. Superseded by install.sh, kept because links to it are in circulation. |
Tag v* to publish a release carrying all six. The release job also runs
install.sh end to end against a throwaway key, so a package that breaks the
quick start fails in CI rather than on a Pi.