Summary
Changing [admin].password in config.toml has no effect on whether a Bearer token issued
by one Memoh instance is accepted by another. Tokens remain valid across instances because they
are signed with [auth].jwt_secret, and every shipped config template hardcodes the same
static placeholder secret.
Root cause
JWT signing/verification depends only on [auth].jwt_secret (HS256, see
internal/auth/jwt.go:GenerateToken / JWTMiddleware). Token claims contain only the account
UUID (sub/user_id) plus iat/exp — nothing derived from the admin password, username, or
any per-deployment value ([admin].password is used solely for bcrypt verification at login,
see cmd/agent/app.go:ensureAdminUser).
The shipped templates all ship with fixed, repeated jwt_secret values, and there is no code
path that generates or persists a random secret per install:
config.toml:17 jwt_secret = "YZq8kXrW5dFpNt9mLxQvHbRjKsMnOePw"
conf/app.docker.toml:17 jwt_secret = "YZq8kXrW5dFpNt9mLxQvHbRjKsMnOePw" # identical
conf/app.apple.toml:17 / app.windows.toml:17 → "memoh-dev-secret-do-not-use-in-production"
conf/app.local.toml:18 → "LOCAL-DEV-CHANGE-ME"
conf/app.example.toml:18 → "CHANGE-ME-TO-A-RANDOM-SECRET"
So any two deployments that copy a template without manually changing jwt_secret (an easy
thing to miss — operators naturally focus on [admin].password) end up with identical HMAC
signing keys, making their Bearer tokens mutually valid.
Risk / Impact
- Cross-instance token reuse: a token minted on instance A authenticates successfully on
instance B (and vice versa) whenever both share a default jwt_secret.
- Token forgery: because these placeholder secrets are committed in a public repo, anyone
who has seen the source can compute HMAC-SHA256(claims, "<placeholder secret>") themselves
and forge a valid Bearer token for an arbitrary user_id without ever logging in — a full
authentication bypass for any deployment that didn't override jwt_secret.
- This is worse than a weak default password, because the existing safeguard for that
(cmd/agent/app.go:1110-1112, which warns when [admin].password == "change-your-password-here")
has no equivalent for jwt_secret.
Steps to reproduce
- Stand up two Memoh instances from the shipped
config.toml (or conf/app.docker.toml),
changing only [admin].username/[admin].password between them.
- Log in to instance A, capture the returned Bearer token.
- Send an authenticated request to instance B using instance A's token.
- Observe instance B accepts the token despite different admin credentials.
Suggested fix (any of)
- Generate a cryptographically random
jwt_secret on first boot when empty/unset, persist it,
and refuse to fall back to a baked-in default.
- At minimum, mirror the existing
change-your-password-here check (cmd/agent/app.go:1110-1112):
detect when jwt_secret matches one of the known shipped placeholder values and refuse to
start (or loudly warn).
- Stop shipping a real secret value in templates; ship
"" and fail fast at boot
(internal/boot/runtime.go:ProvideRuntimeConfig already errors on an empty secret), forcing
operators to generate their own.
Summary
Changing
[admin].passwordinconfig.tomlhas no effect on whether a Bearer token issuedby one Memoh instance is accepted by another. Tokens remain valid across instances because they
are signed with
[auth].jwt_secret, and every shipped config template hardcodes the samestatic placeholder secret.
Root cause
JWT signing/verification depends only on
[auth].jwt_secret(HS256, seeinternal/auth/jwt.go:GenerateToken/JWTMiddleware). Token claims contain only the accountUUID (
sub/user_id) plusiat/exp— nothing derived from the admin password, username, orany per-deployment value (
[admin].passwordis used solely for bcrypt verification at login,see
cmd/agent/app.go:ensureAdminUser).The shipped templates all ship with fixed, repeated
jwt_secretvalues, and there is no codepath that generates or persists a random secret per install:
config.toml:17 jwt_secret = "YZq8kXrW5dFpNt9mLxQvHbRjKsMnOePw"
conf/app.docker.toml:17 jwt_secret = "YZq8kXrW5dFpNt9mLxQvHbRjKsMnOePw" # identical
conf/app.apple.toml:17 / app.windows.toml:17 → "memoh-dev-secret-do-not-use-in-production"
conf/app.local.toml:18 → "LOCAL-DEV-CHANGE-ME"
conf/app.example.toml:18 → "CHANGE-ME-TO-A-RANDOM-SECRET"
So any two deployments that copy a template without manually changing
jwt_secret(an easything to miss — operators naturally focus on
[admin].password) end up with identical HMACsigning keys, making their Bearer tokens mutually valid.
Risk / Impact
instance B (and vice versa) whenever both share a default
jwt_secret.who has seen the source can compute
HMAC-SHA256(claims, "<placeholder secret>")themselvesand forge a valid Bearer token for an arbitrary
user_idwithout ever logging in — a fullauthentication bypass for any deployment that didn't override
jwt_secret.(
cmd/agent/app.go:1110-1112, which warns when[admin].password == "change-your-password-here")has no equivalent for
jwt_secret.Steps to reproduce
config.toml(orconf/app.docker.toml),changing only
[admin].username/[admin].passwordbetween them.Suggested fix (any of)
jwt_secreton first boot when empty/unset, persist it,and refuse to fall back to a baked-in default.
change-your-password-herecheck (cmd/agent/app.go:1110-1112):detect when
jwt_secretmatches one of the known shipped placeholder values and refuse tostart (or loudly warn).
""and fail fast at boot(
internal/boot/runtime.go:ProvideRuntimeConfigalready errors on an empty secret), forcingoperators to generate their own.