lazysite ships with built-in cookie-based authentication as the default
path. The same mechanism supports drop-in replacement by any external
auth proxy that sets X-Remote-* headers (Authentik, Authelia, etc.).
The processor reads the same auth headers regardless of which model is in use. Protected pages, group checks, and TT variables behave identically.
lazysite-auth.pl authenticates users against a flat-file user
database, sets a signed HMAC cookie on success, and translates that
cookie into X-Remote-User/X-Remote-Groups headers for the
processor on subsequent requests.
On localhost, a user entry with no password hash allows password-less sign-in. This is a development convenience; in production, every account must have a password.
Configure Apache to route requests through the auth wrapper before the processor:
FallbackResource /cgi-bin/lazysite-auth.pl
The auth wrapper reads the cookie, populates auth headers, and hands
off to lazysite-processor.pl if the request is authenticated (or
public).
Use the manager Users page, or the lazysite-users.pl CLI:
perl tools/lazysite-users.pl --docroot /path/to/public_html \
add alice secretpassword
perl tools/lazysite-users.pl --docroot /path/to/public_html \
group-add alice admins
add USERNAME PASSWORD Add a new user
passwd USERNAME NEWPASSWORD Change password
remove USERNAME Remove user and group memberships
list List all users
group-add USERNAME GROUP Add user to group
group-remove USERNAME GROUP Remove user from group
groups List all groups and members
Users (lazysite/auth/users):
alice:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
bob:5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5
Each line is username:sha256hex. Lines starting with # are comments.
A user line with no hash (just username:) allows passwordless sign-in
on localhost only.
Groups (lazysite/auth/groups):
admins: alice
lazysite-admins: alice
editors: alice, bob
members: alice, bob, carol
Each line is groupname: user1, user2, ....
The users file is plain text with SHA256 hex hashes. Generate a password hash:
echo -n 'mypassword' | sha256sum | cut -d' ' -f1
Add a user by appending to the file:
echo "alice:$(echo -n 'mypassword' | sha256sum | cut -d' ' -f1)" \
>> lazysite/auth/users
Or with Perl (if sha256sum is not available):
perl -MDigest::SHA=sha256_hex -e 'print sha256_hex("mypassword")'
Groups are plain text too - edit lazysite/auth/groups in any text
editor. Set permissions after editing:
chmod 640 lazysite/auth/users
chmod 644 lazysite/auth/groups
The starter includes login.md and logout.md. The login form POSTs
to /login and logout is at /logout. On successful login a signed
cookie is set and the user is redirected to the original page (via the
next parameter).
The HMAC secret lives at lazysite/auth/.secret (chmod 0660 -
owner + group, never world, so the site user's CLI tools and the
web-server CGI can both use it whichever minted it first).
A session is the signed cookie itself - there is no server-side session
store on the request path. Two small side files make sessions visible and
revocable: at login the cookie payload carries a random session id and one
line (who / when / IP / device) is appended to
lazysite/auth/sessions.jsonl (self-pruned after 24 h), and cookie
verification consults lazysite/auth/revoked.json when it exists. The
manager Sessions page lists the live sessions and can sign out a single
session, all of one user's sessions ("Sign out everywhere"), or - by
rotating the signing secret - everyone at once. Losing the registry only
degrades the listing; cookies minted before this feature cannot be listed
but are still killable per-user or by rotation.
The dev server auto-detects built-in auth when lazysite/auth/users
exists and uses the auth wrapper automatically.
The operator creates an account and sets its parameters; the user provisions their own secret. The operator never sets or handles a password. One primitive underlies every flow: a single-use, short-lived, hashed claim token.
On the Users page, an account card has Generate setup link next to Generate
credential. It mints a claim and shows a one-time URL (…/claim?u=<user>&c=<code>)
to hand over by any channel. The user opens it and the /claim page presents a
set-a-password form (interactive account) or a mint-and-reveal-token action
(machine account). The claim is consumed on success and expires after 24 h.
ui off) accounts cannot redeem a
set-password claim.Where the SMTP plugin is configured and the account has an email, /login shows
a Forgot password? link → /forgot takes a username or email and mails a
set-password claim. The response is identical whether or not an account matched -
it never reveals whether an account or email exists. The reset email is recorded
in the audit trail (action forgot) against the matched account.
An interactive account can enrol TOTP two-factor (RFC 6238). Enrolment shows a
shared secret + an otpauth:// URI (QR) and issues one-time recovery codes;
after enrolment, login requires a valid 6-digit code (or a recovery code) before
the cookie is issued. Two-factor applies to interactive (password → cookie) login
only - token / WebDAV / connector auth is unchanged, since the token is already
the strong factor there.
# enrol from the CLI (or via the Users page card action)
perl tools/lazysite-users.pl --docroot /path/to/public_html mfa-enroll alice
The shared secret lives in user-settings.json under the same 0640/2770
protection as other credentials (the auth dir is off the web and group-restricted;
no at-rest encryption - an accepted tradeoff for self-hosting).
An account may carry expires_at (an epoch); after it, all authentication for
that account fails - time-boxed access for a contractor or a temporary partner.
Distinct from token expiry.
/forgot, /claim, and the partner exchange
never reveal whether an account, email, or claim is valid beyond success/failure./dav).claim-redeem, forgot, token-exchange,
token-rotate, user-claim-create, user-mfa-enroll, and the OAuth events
(oauth-register, oauth-authorize, oauth-refresh, connect).Set auth: in front matter:
---
title: Members Area
auth: required
---
Values:
required - user must be authenticated. Unauthenticated requests
redirect to the login page.optional - auth headers are read if present but access is not
restricted. Use for pages that show different content to logged-in
users.none - no auth check. This is the default.---
title: Admin Dashboard
auth: required
auth_groups:
- admins
- editors
---
The user must be authenticated AND in at least one listed group. Users in the wrong group see the 403 page.
Set auth_default: in lazysite/lazysite.conf:
auth_default: required
Pages without auth: in front matter inherit this value. Default
is none when not set. The login page is always accessible
regardless of the site-wide default.
The manager at /manager uses the same auth mechanism. Access is the
ui capability, granted through a group on the manager Groups page
(the seeded lazysite-admins group carries it):
manager: enabled
manager_path: /manager
Capabilities on groups are the mechanism of record. (The legacy
manager_groups: conf key is retired: on upgrade any group it named receives
its capabilities explicitly and the conf line is removed.)
These variables are available in page content and the view template:
[% authenticated %] - 1 if user is logged in, 0 otherwise[% auth_user %] - username[% auth_name %] - display name (from users file or proxy header)[% auth_email %] - email (from proxy header)[% auth_groups %] - array of group namesExample in a view template:
[% IF authenticated %]
<span>Signed in as [% auth_user %]</span>
<a href="/logout">Sign out</a>
[% ELSE %]
<a href="/login">Sign in</a>
[% END %]
Create 403.md in the docroot. These context variables are available:
[% auth_denied_reason %] - insufficient_groups when group check fails[% auth_required_groups %] - array of required group names[% auth_user %] - the authenticated username[% auth_name %] - display nameThe 403 page is never cached.
Any reverse proxy that sets HTTP headers works with lazysite. The processor reads these headers by default:
X-Remote-User - usernameX-Remote-Name - display nameX-Remote-Email - email addressX-Remote-Groups - comma-separated group listIf your proxy uses different header names, configure them in
lazysite/lazysite.conf:
auth_header_user: Remote-User
auth_header_name: Remote-Name
auth_header_email: Remote-Email
auth_header_groups: Remote-Groups
# In Authentik proxy provider - forwarded headers:
# X-Remote-User: %(username)s
# X-Remote-Name: %(name)s
# X-Remote-Email: %(email)s
# X-Remote-Groups: %(groups|join(","))s
Apache with Authentik:
<Location />
RequestHeader set X-Remote-User "%{AUTHENTIK_USERNAME}e"
RequestHeader set X-Remote-Groups "%{AUTHENTIK_GROUPS}e"
</Location>
Configure header names in lazysite.conf to match Authelia:
auth_header_user: Remote-User
auth_header_name: Remote-Name
auth_header_email: Remote-Email
auth_header_groups: Remote-Groups
nginx with Authelia:
location / {
auth_request /authelia;
auth_request_set $remote_user $upstream_http_remote_user;
auth_request_set $remote_groups $upstream_http_remote_groups;
proxy_set_header X-Remote-User $remote_user;
proxy_set_header X-Remote-Groups $remote_groups;
}
Protected pages (auth: required or with auth_groups:) are never
cached to disk and always include Cache-Control: no-store, private
in the response. This prevents authenticated content from being
served to unauthenticated users.