Every fresh install seeds an admin account with the public password 'ipodderx', usable through the unauthenticated login endpoint #321

Closed
opened 2026-10-23 05:26:43 -07:00 by rays · 2 comments
Owner

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.

When the daemon starts and the users table is empty, it creates the account 'admin' with is_admin=true and the hardcoded password 'ipodderx' (main.rs:597-603). The same credential is published in README.md, docs/users.md and the sample docker-compose.yml. POST /api/login is routed outside the auth middleware (web.rs:88-89), and Caddy passes every /api path to ipx, so an anonymous caller who reaches the page's port can post admin/ipodderx. check_password verifies it like any other password, and a full admin session cookie is issued. No configuration switch disables password sign-in, nothing forces a change or refuses the default at login, and web/src has no warning about it, although the comment at main.rs:121-122 says the settings page nags. The only notice is a log warning that prints the password. Proxy sign-in (Cloudflare Access) does not remove the seeded account, so anyone who can reach the web port directly (the LAN address, or 8099 published on all interfaces as in the sample compose) gets admin authority until the owner changes the password: accounts, server settings, logs and directory administration. The seeding runs again on any later start that finds the users table empty. Whether production still has the default is a deployment fact.

Claimed root cause. First-run bootstrap uses a fixed, documented credential (main.rs:123, 597-603) instead of a random one-time secret, and the login path neither refuses the default nor requires it to be changed.

Local check. {'local': 'In a sandboxed scratch daemon on an empty SQLite database under a scratch directory (as the browser suite does under /tmp/ipx-ui-test), with no external network, start ipx daemon and stop it by its PID afterwards. POST /api/login with {"name":"admin","password":"ipodderx"} and confirm a 200 with admin:true and an ipx_session cookie. Then GET /api/users with that cookie and confirm admin access. Regression test after the fix (a random one-time password printed once, or a must-change flag that limits a default-password session to changing the password): the same login must not give a full admin session.', 'deployment': "The owner checks on Tower, without probing from outside, whether production's 'admin' account still verifies against 'ipodderx'. One way is to run argon2 verify against users.pass_hash for 'admin' inside a read-only transaction. The owner also confirms that LAN port 8099 cannot be reached from untrusted networks."}

Owner check (no probing). The owner checks on Tower, without probing from outside, whether production's 'admin' account still verifies against 'ipodderx'. One way is to run argon2 verify against users.pass_hash for 'admin' inside a read-only transaction. The owner also confirms that LAN port 8099 cannot be reached from untrusted networks.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint crates/ipx/src/main.rs:DEFAULT_PASSWORD:seeded-admin-credential).

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it. When the daemon starts and the users table is empty, it creates the account 'admin' with is_admin=true and the hardcoded password 'ipodderx' (main.rs:597-603). The same credential is published in README.md, docs/users.md and the sample docker-compose.yml. POST /api/login is routed outside the auth middleware (web.rs:88-89), and Caddy passes every /api path to ipx, so an anonymous caller who reaches the page's port can post admin/ipodderx. check_password verifies it like any other password, and a full admin session cookie is issued. No configuration switch disables password sign-in, nothing forces a change or refuses the default at login, and web/src has no warning about it, although the comment at main.rs:121-122 says the settings page nags. The only notice is a log warning that prints the password. Proxy sign-in (Cloudflare Access) does not remove the seeded account, so anyone who can reach the web port directly (the LAN address, or 8099 published on all interfaces as in the sample compose) gets admin authority until the owner changes the password: accounts, server settings, logs and directory administration. The seeding runs again on any later start that finds the users table empty. Whether production still has the default is a deployment fact. **Claimed root cause.** First-run bootstrap uses a fixed, documented credential (main.rs:123, 597-603) instead of a random one-time secret, and the login path neither refuses the default nor requires it to be changed. **Local check.** {'local': 'In a sandboxed scratch daemon on an empty SQLite database under a scratch directory (as the browser suite does under /tmp/ipx-ui-test), with no external network, start `ipx daemon` and stop it by its PID afterwards. POST /api/login with {"name":"admin","password":"ipodderx"} and confirm a 200 with admin:true and an ipx_session cookie. Then GET /api/users with that cookie and confirm admin access. Regression test after the fix (a random one-time password printed once, or a must-change flag that limits a default-password session to changing the password): the same login must not give a full admin session.', 'deployment': "The owner checks on Tower, without probing from outside, whether production's 'admin' account still verifies against 'ipodderx'. One way is to run argon2 verify against users.pass_hash for 'admin' inside a read-only transaction. The owner also confirms that LAN port 8099 cannot be reached from untrusted networks."} **Owner check (no probing).** The owner checks on Tower, without probing from outside, whether production's 'admin' account still verifies against 'ipodderx'. One way is to run argon2 verify against users.pass_hash for 'admin' inside a read-only transaction. The owner also confirms that LAN port 8099 cannot be reached from untrusted networks. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `crates/ipx/src/main.rs:DEFAULT_PASSWORD:seeded-admin-credential`).
rays added the bug label 2026-10-23 05:26:43 -07:00
Author
Owner

Proposed fix:

  1. When the users table is empty, create admin with a random password of about 20 characters, not DEFAULT_PASSWORD.
  2. Write it to /config/admin-password, mode 600. The startup log says where the file is, not what the password is (as for the web token, #40). The current warning prints the password, which puts it in Loki; remove that.
  3. If IPX_ADMIN_PASSWORD is set, use it instead, for scripted installs and the sample compose file.
  4. Update docker-compose.yml, README.md and docs/users.md, which say to sign in as admin / ipodderx: read the password with docker exec iPX cat /config/admin-password, then change it.
  5. Test: a fresh daemon rejects admin / ipodderx and accepts the generated password.

Existing installs are unaffected, since seeding only runs on an empty users table. If production's admin still has ipodderx, it needs a manual ipx passwd.

Not chosen: a first-run setup page (a race on any exposed port) and a must-change flag (leaves a known credential live until someone signs in). The settings-page nag mentioned in the comment at main.rs:121 was never built and is a separate change.

Proposed fix: 1. When the users table is empty, create `admin` with a random password of about 20 characters, not `DEFAULT_PASSWORD`. 2. Write it to `/config/admin-password`, mode 600. The startup log says where the file is, not what the password is (as for the web token, #40). The current warning prints the password, which puts it in Loki; remove that. 3. If `IPX_ADMIN_PASSWORD` is set, use it instead, for scripted installs and the sample compose file. 4. Update `docker-compose.yml`, `README.md` and `docs/users.md`, which say to sign in as admin / ipodderx: read the password with `docker exec iPX cat /config/admin-password`, then change it. 5. Test: a fresh daemon rejects admin / ipodderx and accepts the generated password. Existing installs are unaffected, since seeding only runs on an empty users table. If production's `admin` still has ipodderx, it needs a manual `ipx passwd`. Not chosen: a first-run setup page (a race on any exposed port) and a must-change flag (leaves a known credential live until someone signs in). The settings-page nag mentioned in the comment at main.rs:121 was never built and is a separate change.
Author
Owner

Fixed in cc3b874, as proposed above: a database with no accounts gets admin with IPX_ADMIN_PASSWORD, or a random 24-character password in admin-password beside config.toml (created 0600). The log says where, never what. README, docs/users.md, docs/cli.md, docs/configuration.md and the sample compose file are updated. Deployed; production already has accounts, so nothing changed there. Whether production's own admin still has 'ipodderx' is for the owner to check.

Fixed in cc3b874, as proposed above: a database with no accounts gets admin with IPX_ADMIN_PASSWORD, or a random 24-character password in admin-password beside config.toml (created 0600). The log says where, never what. README, docs/users.md, docs/cli.md, docs/configuration.md and the sample compose file are updated. Deployed; production already has accounts, so nothing changed there. Whether production's own admin still has 'ipodderx' is for the owner to check.
rays closed this issue 2026-10-23 06:56:57 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#321