183 lines
14 KiB
Markdown
183 lines
14 KiB
Markdown
# Firebase Admin: administrator command sequence
|
|
|
|
Prepared only: **none of the privileged commands below has been executed by the agent.** No rebuild is needed. Do not paste passwords, keys, credential JSON, tokens, HAR files, or “Copy as cURL” output into chat. Never enable shell tracing (`set -x`) during this procedure.
|
|
|
|
This sequence is deliberately scoped to the known credential `/etc/motorsignal/firebase-admin.json`, website user `motorsignal`, and deployed revision `8b3a7aa29290b58a9456a564785e9c73cd8e3093`. It stops rather than overwriting configuration if the known baseline has changed. If a different credential or identity is required, stop for a reviewed provisioning plan; do not create a new key or loosen file permissions.
|
|
|
|
## 0. Transfer the non-secret probe from your Windows computer
|
|
|
|
Run in **local PowerShell**, from `C:\Users\Bill\Desktop\flutter-app`. This only uploads diagnostic source; it does not contain credentials or change services.
|
|
|
|
```powershell
|
|
scp -i C:/Users/Bill/.ssh/motorsignal_prod output/deployment/20261004-firebase-admin/admin/probe-firebase-admin.mjs motorsignal-admin@89.31.239.10:/home/motorsignal-admin/probe-firebase-admin.mjs
|
|
ssh -i C:/Users/Bill/.ssh/motorsignal_prod motorsignal-admin@89.31.239.10
|
|
```
|
|
|
|
Review the uploaded file, then use your existing secure administrator access on this VPS. `sudo` authentication stays in your own terminal; never send it to the agent. Commands below use ordinary SSH plus interactive sudo, not a new root SSH login or new access grant.
|
|
|
|
## 1. Read-only validation; no configuration edits or restart
|
|
|
|
Run in your **VPS SSH shell**:
|
|
|
|
```bash
|
|
set +x
|
|
systemctl show motorsignal-website.service -p User -p ActiveState -p EnvironmentFiles -p DropInPaths
|
|
sudo stat -c '%U %G %a %n' /etc/motorsignal/firebase-admin.json
|
|
sudo -u motorsignal env -i PATH=/usr/bin:/bin /usr/bin/node --input-type=module - metadata < /home/motorsignal-admin/probe-firebase-admin.mjs
|
|
```
|
|
|
|
The probe reads the existing file as the website user. It prints only type, project ID, service-account email, mode, and readability. It requires a regular file owned by that user, no world permissions, and no group write/execute permission. It requires project `motoapp-d22e0`. Failure means **STOP**; do not `chmod` the shared credential to bypass it.
|
|
|
|
### Identity-owner approval and IAM review: separate gates
|
|
|
|
In your **secure Google Cloud Console administrator session**, select `motoapp-d22e0`, locate the exact service-account email printed above, and verify that the account is enabled. Review its existing project/database/inherited grants and conditions. Confirm effective `datastore.entities.get` and `datastore.entities.list` on the production database. Inspect any unexpectedly broad grants with the owner; do not automatically reuse an unrelated identity merely because it can read data.
|
|
|
|
Obtain explicit approval from the identity owner for **website runtime reuse of this exact identity**. Record the approver, identity email, purpose, scope, and approval reference in your normal secure operations record. IAM access and a routing-service reference do **not** themselves constitute reuse approval. No command below grants IAM roles, changes rules, or creates a key.
|
|
|
|
Firestore server SDK reads use IAM, not the website client's security rules. `roles/datastore.viewer` includes document get/list, but it permits reads across Firestore resources; it is a reference for reviewing existing access, **not a command to broaden grants**. [Google Firestore IAM reference](https://docs.cloud.google.com/firestore/native/docs/security/iam)
|
|
|
|
After owner approval, run in your **VPS SSH shell**. Enter only the non-secret email and approval reference:
|
|
|
|
```bash
|
|
read -r -p 'Approved service-account email: ' APPROVED_IDENTITY
|
|
read -r -p 'Identity-owner approval reference: ' APPROVAL_REFERENCE
|
|
test -n "$APPROVED_IDENTITY" && test -n "$APPROVAL_REFERENCE"
|
|
sudo -u motorsignal env -i PATH=/usr/bin:/bin /usr/bin/node --input-type=module - probe "$APPROVED_IDENTITY" < /home/motorsignal-admin/probe-firebase-admin.mjs
|
|
```
|
|
|
|
Require `firestoreReadProbes: PASS`. The probe exercises ADC using the deployed website's installed Firebase Admin SDK: the actual public/scheduled-or-assembling community query shape, limited to one result, plus two document reads on the exact block/participant path shapes. The latter use the fixed name `website-credential-read-probe`; missing documents are acceptable and **no document is created**. No document contents or returned IDs are printed, and no collection-group scans are performed. These probes check get/list access; they do not prove every route authorization branch or establish human approval. A permission, disabled/invalid credential, missing-index, ownership, or timeout error means **STOP**. Review privately; do not create indexes, fixtures, or keys to force success in this task.
|
|
|
|
The probe uses clean process settings and checks file readability, but the post-restart browser check below remains required to establish access from the actual website process. [Firebase ADC setup](https://firebase.google.com/docs/admin/setup)
|
|
|
|
## 2. Configuration changes: only after both gates pass
|
|
|
|
Run this block in your **VPS SSH shell using your secure interactive sudo access**. It rechecks approval and Firestore reads, preserves the baseline, creates only a website-specific environment file/drop-in, and restarts only the website. It does not touch the shared credential, routing environment, backend environment, code, or build assets.
|
|
|
|
The fixed backup path is `/var/backups/motorsignal-website/firebase-admin-20261004`. If it already exists, the block stops: do not reuse or overwrite it. If any failure occurs after the backup is created, use section 4 instead of blindly rerunning.
|
|
|
|
```bash
|
|
sudo /bin/bash -s -- "$APPROVED_IDENTITY" "$APPROVAL_REFERENCE" <<'ADMIN'
|
|
set -euo pipefail
|
|
set +x
|
|
umask 077
|
|
test "$#" -eq 2 && test -n "$1" && test -n "$2"
|
|
read -r -p 'Owner approved this exact identity for website reuse? Type APPROVED: ' ANSWER </dev/tty
|
|
test "$ANSWER" = APPROVED
|
|
UNIT=/etc/systemd/system/motorsignal-website.service
|
|
ENV_FILE=/etc/motorsignal/website.env
|
|
DROP_DIR=/etc/systemd/system/motorsignal-website.service.d
|
|
DROP_FILE="$DROP_DIR/10-firebase-admin.conf"
|
|
BACKUP=/var/backups/motorsignal-website/firebase-admin-20261004
|
|
|
|
# Refuse drift, symlinks, existing target files, or competing runtime configuration.
|
|
test ! -L "$UNIT" && test ! -L /etc/motorsignal
|
|
test "$(sha256sum "$UNIT" | cut -d ' ' -f1)" = 68ed666be77f15f0d9eb9bbfeffff7499c0e0c9618a42e57243ac554aa76d182
|
|
test -z "$(systemctl show motorsignal-website.service -p DropInPaths --value)"
|
|
test -z "$(systemctl show motorsignal-website.service -p EnvironmentFiles --value)"
|
|
test ! -e "$ENV_FILE" && test ! -L "$ENV_FILE"
|
|
test ! -e "$DROP_DIR" && test ! -L "$DROP_DIR"
|
|
test -z "$(find /opt/motorsignal/current/website -maxdepth 1 -name '.env*' -print -quit)"
|
|
systemctl show motorsignal-website.service -p Environment --value | /usr/bin/python3 -c 'import shlex,sys; e=dict(s.split("=",1) for s in shlex.split(sys.stdin.read()) if "=" in s); assert set(e)=={"NODE_ENV","HOSTNAME","PORT","MOTORSIGNAL_API_BASE_URL"}; assert e["NODE_ENV"]=="production" and e["PORT"]=="3001"'
|
|
curl --fail --silent --show-error --max-time 15 https://motorsignal.app/api/version | /usr/bin/python3 -c 'import json,sys; assert json.load(sys.stdin)["revision"]=="8b3a7aa29290b58a9456a564785e9c73cd8e3093"'
|
|
sudo -u motorsignal env -i PATH=/usr/bin:/bin /usr/bin/node --input-type=module - probe "$1" < /home/motorsignal-admin/probe-firebase-admin.mjs
|
|
|
|
# All validation above must succeed before configuration or backup writes.
|
|
if test -e /var/backups/motorsignal-website; then
|
|
test ! -L /var/backups/motorsignal-website
|
|
test "$(stat -c '%u %a' /var/backups/motorsignal-website)" = '0 700'
|
|
else
|
|
mkdir -m 0700 /var/backups/motorsignal-website
|
|
fi
|
|
mkdir -m 0700 "$BACKUP"
|
|
cp --preserve=all "$UNIT" "$BACKUP/unit.before"
|
|
printf '%s\n' 'website.env absent' '10-firebase-admin.conf absent' 'drop-in directory absent' > "$BACKUP/prior-state.txt"
|
|
printf 'identity=%s\napproval_reference=%s\n' "$1" "$2" > "$BACKUP/approval.txt"
|
|
printf '%s\n' 'FIREBASE_PROJECT_ID=motoapp-d22e0' 'GOOGLE_APPLICATION_CREDENTIALS=/etc/motorsignal/firebase-admin.json' > "$BACKUP/website.env.applied"
|
|
printf '%s\n' '[Service]' 'EnvironmentFile=/etc/motorsignal/website.env' > "$BACKUP/10-firebase-admin.conf.applied"
|
|
date -u '+%Y-%m-%d %H:%M:%S UTC' > "$BACKUP/restart-since.txt"
|
|
printf 'Protected backup: %s\nRollback: section 4 of the administrator runbook\n' "$BACKUP"
|
|
|
|
install -m 0640 -o root -g motorsignal "$BACKUP/website.env.applied" "$ENV_FILE"
|
|
install -d -m 0755 -o root -g root "$DROP_DIR"
|
|
install -m 0644 -o root -g root "$BACKUP/10-firebase-admin.conf.applied" "$DROP_FILE"
|
|
sudo -u motorsignal test -r "$ENV_FILE"
|
|
sudo -u motorsignal test -r /etc/motorsignal/firebase-admin.json
|
|
systemctl daemon-reload
|
|
systemctl restart motorsignal-website.service
|
|
systemctl is-active --quiet motorsignal-website.service
|
|
printf '%s\n' 'Website restarted. Authenticated browser and post-restart log checks are still required.'
|
|
ADMIN
|
|
```
|
|
|
|
Do not run concurrently with another administrator editing website configuration. The prior unit is backed up but never rewritten. The added environment file contains only project/path settings; credential JSON remains in its existing protected file. No Firebase credentials belong in `.next`, `public`, `next.config`, `NEXT_PUBLIC_*`, shell history, or browser assets.
|
|
|
|
## 3. Read-only post-restart verification
|
|
|
|
First run in your **VPS SSH shell** (no sudo required for HTTP checks):
|
|
|
|
```bash
|
|
/usr/bin/node --input-type=module - <<'HTTP'
|
|
const expected = '8b3a7aa29290b58a9456a564785e9c73cd8e3093';
|
|
const version = await fetch('https://motorsignal.app/api/version', { cache:'no-store', signal:AbortSignal.timeout(15000) });
|
|
if (version.status !== 200 || (await version.json()).revision !== expected) throw new Error('revision_check_failed');
|
|
const response = await fetch('https://motorsignal.app/api/group-rides', { cache:'no-store', signal:AbortSignal.timeout(15000) });
|
|
if (response.status !== 401 || (await response.json()).error !== 'unauthorized') throw new Error('unauthenticated_401_failed');
|
|
console.log('PASS: unchanged revision; unauthenticated discovery 401');
|
|
HTTP
|
|
```
|
|
|
|
Then, in your **existing authorized production browser session**, open `https://motorsignal.app/group-rides/manage` and select **Explore Community Rides** (or **Retry Loading**). In that browser's Network panel, inspect the actual GET `/api/group-rides` response: require HTTP 200 and JSON with `rides` as an array and `nextCursor` as null or a string. Keep request headers/tokens in the browser; do not export them or copy them into a shell. This is the authenticated end-to-end check; the service-account probe is not a substitute.
|
|
|
|
If the actual eligible result is empty, require **No Community Rides Scheduled**, not **Community rides could not be loaded**. If results are populated, verify that view and record the natural empty-result check as not exercised; do not create/delete rides or mock a production response. If pagination remains, do not claim the entire candidate set is empty from only the first page. A 401, 403, 500, or malformed response means the task is not verified.
|
|
|
|
After that authenticated request, run in your **VPS SSH shell**:
|
|
|
|
```bash
|
|
sudo /bin/bash -s <<'LOGS'
|
|
set -euo pipefail
|
|
set +x
|
|
SINCE=$(cat /var/backups/motorsignal-website/firebase-admin-20261004/restart-since.txt)
|
|
systemctl is-active --quiet motorsignal-website.service
|
|
journalctl -u motorsignal-website.service --since "$SINCE" --no-pager | /usr/bin/python3 -c 'import re,sys; s=sys.stdin.read(); assert s.strip() and "-- No entries --" not in s; c=len(re.findall(r"Could not load the default credentials|Failed to parse FIREBASE_SERVICE_ACCOUNT|\[FIREBASE_ADMIN\].*Failed|\[GROUP_RIDE_DISCOVERY\] Failed",s)); print("Post-restart credential/discovery failures:",c); sys.exit(1 if c else 0)'
|
|
LOGS
|
|
```
|
|
|
|
This prints a count, not arbitrary log lines. Zero failures is meaningful only alongside the successful authenticated request. Review any failure privately with administrator access; do not paste raw secret-bearing logs into chat. No Firestore rules or entitlements should be weakened.
|
|
|
|
## 4. Exact configuration rollback (website only)
|
|
|
|
Run in your **VPS SSH shell using secure interactive sudo access**. This also handles partial application. It refuses to overwrite subsequent edits, moves only the two files created above into the protected backup, leaves the shared credential untouched, and restarts only the website. The new empty drop-in directory is intentionally retained; it has no active configuration. No code rollback/rebuild is needed.
|
|
|
|
```bash
|
|
sudo /bin/bash -s <<'ROLLBACK'
|
|
set -euo pipefail
|
|
set +x
|
|
umask 077
|
|
BACKUP=/var/backups/motorsignal-website/firebase-admin-20261004
|
|
UNIT=/etc/systemd/system/motorsignal-website.service
|
|
ENV_FILE=/etc/motorsignal/website.env
|
|
DROP_FILE=/etc/systemd/system/motorsignal-website.service.d/10-firebase-admin.conf
|
|
test -d "$BACKUP" && test ! -L "$BACKUP"
|
|
test "$(stat -c '%u %a' "$BACKUP")" = '0 700'
|
|
test ! -L "$UNIT"
|
|
test ! -L /etc/motorsignal && test ! -L /etc/systemd/system/motorsignal-website.service.d
|
|
cmp --silent "$UNIT" "$BACKUP/unit.before"
|
|
for NAME in website.env 10-firebase-admin.conf; do
|
|
if test "$NAME" = website.env; then TARGET="$ENV_FILE"; else TARGET="$DROP_FILE"; fi
|
|
test ! -L "$TARGET"
|
|
if test -e "$TARGET"; then
|
|
cmp --silent "$TARGET" "$BACKUP/$NAME.applied"
|
|
test ! -e "$BACKUP/$NAME.removed" && test ! -L "$BACKUP/$NAME.removed"
|
|
fi
|
|
done
|
|
if test -e "$DROP_FILE"; then mv -- "$DROP_FILE" "$BACKUP/10-firebase-admin.conf.removed"; fi
|
|
if test -e "$ENV_FILE"; then mv -- "$ENV_FILE" "$BACKUP/website.env.removed"; fi
|
|
systemctl daemon-reload
|
|
systemctl restart motorsignal-website.service
|
|
systemctl is-active --quiet motorsignal-website.service
|
|
printf '%s\n' 'Prior website configuration restored; shared credential and deployed code unchanged.'
|
|
ROLLBACK
|
|
```
|
|
|
|
Repeat the revision/unauthenticated HTTP checks after rollback. The prior missing-ADC failure is expected to return; rollback restores configuration, not a working credential setup. Retain the protected backup and approval record. No password, key, token, or credential content is needed in chat.
|