From 4512f78033fd84fc45ca25d1ff3351ff98255933 Mon Sep 17 00:00:00 2001 From: Tero Date: Sat, 29 Aug 2026 20:07:00 +0300 Subject: [PATCH] Add sfsr protection and admin pwd change script for powershell --- README.md | 31 ++++++++++++++++++++++++++ TODO.md | 6 ++--- __pycache__/app.cpython-314.pyc | Bin 31073 -> 31158 bytes app.py | 2 ++ requirements.txt | 1 + scripts/rotate-admin.ps1 | 37 +++++++++++++++++++++++++++++++ scripts/test-smtp.ps1 | 38 ++++++++++++++++++++++++++++++++ templates/admin_users.html | 2 +- templates/auth.html | 1 + templates/base.html | 2 +- templates/editor.html | 1 + templates/mfa.html | 1 + templates/password_form.html | 1 + 13 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 scripts/rotate-admin.ps1 create mode 100644 scripts/test-smtp.ps1 diff --git a/README.md b/README.md index a98a32a..60a09c8 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,43 @@ Open `http://localhost:8000`, create the first account, and publish a field note The initial administrator account is `admin` with password `admin`, as requested for first-run access. Sign in, scan the displayed QR code with any iPhone or Android TOTP authenticator, and change this password before exposing the service to the internet. New registrations are held for approval in **Accounts**; accepted users must enroll a TOTP authenticator before they can publish. +### Secure the initial administrator + +With the Docker service running, rotate the initial password locally. The command prompts for a new password without writing it to a file or command history. It also clears the old MFA binding so that the next administrator sign-in requires a new QR-based authenticator enrollment. + +```powershell +.\scripts\rotate-admin.ps1 +``` + +After it prints `ADMIN_PASSWORD_ROTATED_MFA_RESET`, sign in as `admin` with the new password, scan the fresh QR code with an authenticator application, and enter its six-digit code to complete enrollment. Store the new password in a password manager. + ## Email and password recovery Registration now requires an email address. Members can change their password from the navigation. The sign-in page provides an email-based recovery link; it expires after one hour and can only be used once. Administrators can send the same recovery email to any approved member from **Accounts**. +All browser POST forms are protected by server-validated CSRF tokens. + Set `PUBLIC_URL` and the `MAIL_*` values in `.env` to send recovery emails. The SMTP account must support STARTTLS on the configured port. Review [TODO.md](TODO.md) before production deployment. +### Configure SMTP delivery + +1. Copy `.env.example` to `.env` if it does not exist. +2. Set `PUBLIC_URL=https://eternityproject.fi`. +3. Enter the SMTP host, port, username, password or provider app password, and verified sender address. Use port `587` for STARTTLS. +4. Rebuild the service so Compose applies the values: + +```sh +docker compose up --build -d +``` + +5. Send a delivery test to an inbox you control: + +```powershell +.\scripts\test-smtp.ps1 -To you@example.com +``` + +The command prints `SMTP_TEST_SENT_TO=
` only after the SMTP server accepts the message. Confirm the message arrives, then use **Forgot your password?** in the application to verify a real reset email and link. + ## Production notes Put this service behind a TLS reverse proxy for `eternityproject.fi` (for example Caddy or Nginx). Set a strong unique `SECRET_KEY`; the Compose file intentionally refuses to start without it. Back up the `eternity_data` Docker volume, which contains accounts and posts. diff --git a/TODO.md b/TODO.md index 4ee4937..85ab7b6 100644 --- a/TODO.md +++ b/TODO.md @@ -4,8 +4,8 @@ - [x] Local accounts, administrator approval, and TOTP MFA. - [x] QR-based MFA enrollment for iPhone and Android authenticators. - [x] Email addresses, member password changes, and expiring password-reset links. -- [ ] Configure production SMTP credentials and verify outgoing email delivery. -- [ ] Change the initial `admin` password and enroll its authenticator. -- [ ] Add CSRF protection to all state-changing forms. +- [ ] Configure production SMTP credentials and verify outgoing email delivery. See the SMTP delivery steps in [README.md](README.md); complete after the test email is received. +- [ ] Run `./scripts/rotate-admin.ps1`, then sign in as `admin` and scan the new MFA QR code to complete authenticator enrollment. +- [x] Add CSRF protection to all state-changing forms. - [ ] Add automated database backups and test restoration. - [ ] Configure TLS reverse proxy and production domain for `eternityproject.fi`. \ No newline at end of file diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc index 51feb7da042d29bfdacd371e3ae4d3ad443e31fa..916674dd7b3c6209b2ff2ddba1d1fa0e56b0d1ce 100644 GIT binary patch delta 5494 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 z delta 5338 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 user.username }}{% if user.role == 'admin' %} ADMIN{% endif %} {{ user.created_at[:10] }} {{ 'ENABLED' if user.mfa_enabled else 'PENDING' }} - {% if user.is_approved and user.role == 'member' %}
{% elif user.is_approved %}APPROVED{% else %}
{% endif %}
+ {% if user.is_approved and user.role == 'member' %}
{% elif user.is_approved %}APPROVED{% else %}
{% endif %}
{% endfor %} diff --git a/templates/auth.html b/templates/auth.html index 01e02e4..e1514c7 100644 --- a/templates/auth.html +++ b/templates/auth.html @@ -4,6 +4,7 @@
MEMBER ACCESS

{{ 'Join the circuit.' if mode == 'register' else 'Resume the signal.' }}

Accounts let you publish and maintain your own engineering notes.

+ {% if mode == 'register' %}{% endif %} diff --git a/templates/base.html b/templates/base.html index 03a9095..353fe40 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,7 +23,7 @@ {% if current_user.role == 'admin' %}Accounts{% endif %} Password {{ current_user.username }} -
+
{% else %} Sign in Create account diff --git a/templates/editor.html b/templates/editor.html index 5748778..98aa1af 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -3,6 +3,7 @@ {% block content %}
{{ 'EDIT TRANSMISSION' if post else 'NEW TRANSMISSION' }}AUTHOR / {{ current_user.username }}
+
diff --git a/templates/mfa.html b/templates/mfa.html index 798e156..61d9cd4 100644 --- a/templates/mfa.html +++ b/templates/mfa.html @@ -15,6 +15,7 @@ {% endif %} +
diff --git a/templates/password_form.html b/templates/password_form.html index b1c773a..09d228b 100644 --- a/templates/password_form.html +++ b/templates/password_form.html @@ -4,6 +4,7 @@
ACCOUNT SECURITY

{% if mode == 'forgot' %}Recover the
signal.{% elif mode == 'reset' %}Set a new
password.{% else %}Update your
password.{% endif %}

{% if mode == 'forgot' %}Enter the email address connected to your account. A secure reset link will arrive by email.{% else %}Choose a password with at least 10 characters.{% endif %}

+ {% if mode == 'forgot' %}{% endif %} {% if mode == 'change' %}{% endif %} {% if mode != 'forgot' %}{% endif %}