Accept plaintext admin password — hash on startup, clear from memory

- New MEDIARIP__ADMIN__PASSWORD env var accepts plaintext password
- Hashed via bcrypt on startup, plaintext cleared from memory immediately
- PASSWORD_HASH still works for backward compatibility (takes precedence)
- Removes the 'docker run python bcrypt' ceremony from setup flow
- Updated README, docker-compose, .env.example to use plaintext
This commit is contained in:
xpltd 2026-03-21 22:40:34 -05:00
parent bfc7eba03f
commit 2bb97a0b30
6 changed files with 30 additions and 22 deletions

View file

@ -7,13 +7,8 @@
DOMAIN=media.example.com DOMAIN=media.example.com
# ── Admin credentials ── # ── Admin credentials ──
# Username for the admin panel
ADMIN_USERNAME=admin ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme
# Bcrypt password hash — generate with:
# docker run --rm python:3.12-slim python -c \
# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
ADMIN_PASSWORD_HASH=
# ── Session mode (optional) ── # ── Session mode (optional) ──
# isolated = each browser has its own queue (default) # isolated = each browser has its own queue (default)

View file

@ -53,7 +53,7 @@ These are the knobs most operators actually touch — all shown commented out in
| `MEDIARIP__SESSION__MODE` | `isolated` | Set to `shared` for family/team use, `open` to disable sessions entirely | | `MEDIARIP__SESSION__MODE` | `isolated` | Set to `shared` for family/team use, `open` to disable sessions entirely |
| `MEDIARIP__DOWNLOADS__MAX_CONCURRENT` | `3` | Increase for faster connections, decrease on low-spec hardware | | `MEDIARIP__DOWNLOADS__MAX_CONCURRENT` | `3` | Increase for faster connections, decrease on low-spec hardware |
| `MEDIARIP__PURGE__MAX_AGE_MINUTES` | `1440` | Raise for longer retention, or set `PURGE__ENABLED=false` to keep forever | | `MEDIARIP__PURGE__MAX_AGE_MINUTES` | `1440` | Raise for longer retention, or set `PURGE__ENABLED=false` to keep forever |
| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Pre-set to skip the first-run wizard (useful for automated deployments) | | `MEDIARIP__ADMIN__PASSWORD` | _(empty)_ | Pre-set to skip the first-run wizard (useful for automated deployments) |
### All Settings ### All Settings
@ -76,7 +76,8 @@ These are the knobs most operators actually touch — all shown commented out in
|----------|---------|-------------| |----------|---------|-------------|
| `MEDIARIP__ADMIN__ENABLED` | `true` | Enable admin panel | | `MEDIARIP__ADMIN__ENABLED` | `true` | Enable admin panel |
| `MEDIARIP__ADMIN__USERNAME` | `admin` | Admin username | | `MEDIARIP__ADMIN__USERNAME` | `admin` | Admin username |
| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash of admin password | | `MEDIARIP__ADMIN__PASSWORD` | _(empty)_ | Admin password (plaintext — hashed on startup, never stored) |
| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash (alternative to plaintext — for advanced users) |
#### Purge #### Purge
@ -115,17 +116,21 @@ These are the knobs most operators actually touch — all shown commented out in
Enabled by default. On first run, you'll be prompted to set a password in the browser. Enabled by default. On first run, you'll be prompted to set a password in the browser.
To pre-configure for automated deployments (skip the wizard): To pre-configure for automated deployments (skip the wizard), set the password via environment variable or `.env` file:
```yaml
# docker-compose.yml
environment:
- MEDIARIP__ADMIN__PASSWORD=your-password-here
```
```bash ```bash
# Generate a bcrypt hash # Or in .env file
docker run --rm python:3.12-slim python -c \ MEDIARIP__ADMIN__PASSWORD=your-password-here
"import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
# Then set in docker-compose.yml:
# MEDIARIP__ADMIN__PASSWORD_HASH=$2b$12$...your.hash...
``` ```
The plaintext password is hashed on startup and cleared from memory — it's never stored or logged.
### Troubleshooting: YouTube 403 Errors ### Troubleshooting: YouTube 403 Errors
YouTube downloads work out of the box — yt-dlp automatically selects the right player clients. If you do hit HTTP 403 errors, it's usually one of: YouTube downloads work out of the box — yt-dlp automatically selects the right player clients. If you do hit HTTP 403 errors, it's usually one of:

View file

@ -97,7 +97,8 @@ class AdminConfig(BaseModel):
enabled: bool = True enabled: bool = True
username: str = "admin" username: str = "admin"
password_hash: str = "" password: str = "" # Plaintext — hashed on startup, never stored
password_hash: str = "" # Bcrypt hash — set directly or derived from password
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -49,6 +49,17 @@ async def lifespan(app: FastAPI):
config = AppConfig() config = AppConfig()
logger.info("Config loaded from defaults + env vars (no YAML file)") logger.info("Config loaded from defaults + env vars (no YAML file)")
# --- Derive password hash from plaintext if provided ---
if config.admin.password and not config.admin.password_hash:
import bcrypt
config.admin.password_hash = bcrypt.hashpw(
config.admin.password.encode("utf-8"),
bcrypt.gensalt(),
).decode("utf-8")
logger.info("Admin password hashed from plaintext config")
# Clear plaintext from memory — only the hash is needed at runtime
config.admin.password = ""
# --- TLS warning --- # --- TLS warning ---
if config.admin.enabled and config.admin.password_hash: if config.admin.enabled and config.admin.password_hash:
logger.warning( logger.warning(

View file

@ -5,10 +5,6 @@
# Setup: # Setup:
# 1. Copy .env.example to .env and fill in your values # 1. Copy .env.example to .env and fill in your values
# 2. Run: docker compose -f docker-compose.example.yml up -d # 2. Run: docker compose -f docker-compose.example.yml up -d
#
# Generate a bcrypt password hash:
# docker run --rm python:3.12-slim python -c \
# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
services: services:
media-rip: media-rip:
@ -25,7 +21,7 @@ services:
# Admin panel # Admin panel
MEDIARIP__ADMIN__ENABLED: "true" MEDIARIP__ADMIN__ENABLED: "true"
MEDIARIP__ADMIN__USERNAME: "${ADMIN_USERNAME:-admin}" MEDIARIP__ADMIN__USERNAME: "${ADMIN_USERNAME:-admin}"
MEDIARIP__ADMIN__PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}" MEDIARIP__ADMIN__PASSWORD: "${ADMIN_PASSWORD}"
# Session mode: isolated (default), shared, or open # Session mode: isolated (default), shared, or open
MEDIARIP__SESSION__MODE: "${SESSION_MODE:-isolated}" MEDIARIP__SESSION__MODE: "${SESSION_MODE:-isolated}"
expose: expose:

View file

@ -30,7 +30,7 @@ services:
# - MEDIARIP__PURGE__MAX_AGE_MINUTES=1440 # - MEDIARIP__PURGE__MAX_AGE_MINUTES=1440
# #
## Pre-set admin password (skip first-run wizard): ## Pre-set admin password (skip first-run wizard):
# - MEDIARIP__ADMIN__PASSWORD_HASH=$2b$12$... # - MEDIARIP__ADMIN__PASSWORD=changeme
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]