Skip to content

Commit ef6be53

Browse files
Thomas TupperThomas Tupper
authored andcommitted
ops: add environment-agnostic neo4j auth preflight and rotation helpers
1 parent 6ef22f1 commit ef6be53

4 files changed

Lines changed: 154 additions & 0 deletions

File tree

docs/NEO4J_AUTH_RUNBOOK.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Neo4j Auth Runbook (Environment-Agnostic)
2+
3+
## Goals
4+
- Keep a single source of truth in `.env`
5+
- Prevent auth drift between compose config and Neo4j persisted auth state
6+
- Provide deterministic preflight and rotation helpers
7+
8+
## Files
9+
- `scripts/lib/common.sh`
10+
- `scripts/neo4j-auth-preflight.sh`
11+
- `scripts/neo4j-auth-rotate.sh`
12+
13+
## Required env contract
14+
In `.env`:
15+
- `NEO4J_PASSWORD=<secret>`
16+
- `NEO4J_AUTH=neo4j/${NEO4J_PASSWORD}`
17+
18+
## Preflight check
19+
```bash
20+
scripts/neo4j-auth-preflight.sh
21+
```
22+
Expected output: `NEO4J_AUTH_OK`
23+
24+
## Rotate password safely
25+
```bash
26+
scripts/neo4j-auth-rotate.sh --new-pass 'your-new-strong-password'
27+
```
28+
Expected output:
29+
- `NEO4J_AUTH_OK`
30+
- `NEO4J_PASSWORD_ROTATED_OK`
31+
32+
## Notes
33+
- These scripts avoid hardcoded host paths by deriving compose/env paths from script location, with overrides via `PROJECT_DIR`, `COMPOSE_FILE`, and `ENV_FILE`.
34+
- If your container names differ, set `NEO4J_CONTAINER` before running.

scripts/lib/common.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PROJECT_DIR_DEFAULT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
PROJECT_DIR="${PROJECT_DIR:-$PROJECT_DIR_DEFAULT}"
6+
COMPOSE_FILE="${COMPOSE_FILE:-$PROJECT_DIR/compose.graph.yml}"
7+
ENV_FILE="${ENV_FILE:-$PROJECT_DIR/.env}"
8+
SERVICE_NEO4J="${SERVICE_NEO4J:-neo4j}"
9+
SERVICE_STORE="${SERVICE_STORE:-store}"
10+
11+
require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1" >&2; exit 1; }; }
12+
13+
require_env() {
14+
local k="$1"
15+
if ! grep -q "^${k}=" "$ENV_FILE"; then
16+
echo "missing required env var in $ENV_FILE: $k" >&2
17+
exit 1
18+
fi
19+
}
20+
21+
env_get() {
22+
local k="$1"
23+
grep -E "^${k}=" "$ENV_FILE" | tail -n1 | cut -d= -f2-
24+
}
25+
26+
compose() {
27+
docker compose -f "$COMPOSE_FILE" "$@"
28+
}

scripts/neo4j-auth-preflight.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
source "$(dirname "$0")/lib/common.sh"
4+
5+
require_cmd docker
6+
require_env NEO4J_PASSWORD
7+
PASS="$(env_get NEO4J_PASSWORD)"
8+
CONTAINER="${NEO4J_CONTAINER:-foxmemory-neo4j}"
9+
10+
echo "[preflight] checking neo4j auth via cypher-shell..."
11+
if docker exec "$CONTAINER" cypher-shell -u neo4j -p "$PASS" "RETURN 1 as ok;" >/tmp/neo4j-preflight.out 2>&1; then
12+
echo "NEO4J_AUTH_OK"
13+
else
14+
echo "NEO4J_AUTH_FAIL"
15+
tail -n 20 /tmp/neo4j-preflight.out || true
16+
exit 1
17+
fi

scripts/neo4j-auth-rotate.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
source "$(dirname "$0")/lib/common.sh"
4+
5+
usage() {
6+
cat <<USAGE
7+
Usage: $0 --new-pass '<password>' [--no-restart]
8+
9+
Rotates Neo4j password using compose/.env contract, then verifies login.
10+
USAGE
11+
}
12+
13+
NEW_PASS=""
14+
RESTART=1
15+
while [[ $# -gt 0 ]]; do
16+
case "$1" in
17+
--new-pass) NEW_PASS="$2"; shift 2;;
18+
--no-restart) RESTART=0; shift;;
19+
-h|--help) usage; exit 0;;
20+
*) echo "unknown arg: $1"; usage; exit 1;;
21+
esac
22+
done
23+
24+
[[ -n "$NEW_PASS" ]] || { echo "--new-pass is required"; usage; exit 1; }
25+
require_cmd docker
26+
require_env NEO4J_PASSWORD
27+
28+
# Update .env source-of-truth
29+
if grep -q '^NEO4J_PASSWORD=' "$ENV_FILE"; then
30+
sed -i.bak "s#^NEO4J_PASSWORD=.*#NEO4J_PASSWORD=${NEW_PASS}#" "$ENV_FILE"
31+
else
32+
echo "NEO4J_PASSWORD=${NEW_PASS}" >> "$ENV_FILE"
33+
fi
34+
if grep -q '^NEO4J_AUTH=' "$ENV_FILE"; then
35+
sed -i.bak "s#^NEO4J_AUTH=.*#NEO4J_AUTH=neo4j/${NEW_PASS}#" "$ENV_FILE"
36+
else
37+
echo "NEO4J_AUTH=neo4j/${NEW_PASS}" >> "$ENV_FILE"
38+
fi
39+
40+
echo "[rotate] updated $ENV_FILE"
41+
42+
if [[ "$RESTART" -eq 1 ]]; then
43+
compose stop "$SERVICE_STORE" || true
44+
45+
# Temporarily disable auth, set password, restore auth contract
46+
python3 - <<PY
47+
from pathlib import Path
48+
p=Path("$COMPOSE_FILE")
49+
s=p.read_text()
50+
s=s.replace('NEO4J_AUTH: neo4j/${NEO4J_PASSWORD}','NEO4J_AUTH: none')
51+
p.write_text(s)
52+
PY
53+
54+
compose up -d "$SERVICE_NEO4J"
55+
sleep 10
56+
57+
CONTAINER="${NEO4J_CONTAINER:-foxmemory-neo4j}"
58+
docker exec "$CONTAINER" cypher-shell "ALTER USER neo4j SET PASSWORD '$NEW_PASS' CHANGE NOT REQUIRED;" >/tmp/neo4j-rotate.out 2>&1 || {
59+
echo "[rotate] failed to set password"; tail -n 30 /tmp/neo4j-rotate.out; exit 1;
60+
}
61+
62+
python3 - <<PY
63+
from pathlib import Path
64+
p=Path("$COMPOSE_FILE")
65+
s=p.read_text()
66+
s=s.replace('NEO4J_AUTH: none','NEO4J_AUTH: neo4j/${NEO4J_PASSWORD}')
67+
p.write_text(s)
68+
PY
69+
70+
compose up -d "$SERVICE_NEO4J" "$SERVICE_STORE"
71+
sleep 10
72+
fi
73+
74+
"$(dirname "$0")/neo4j-auth-preflight.sh"
75+
echo "NEO4J_PASSWORD_ROTATED_OK"

0 commit comments

Comments
 (0)