Status: Draft
Updated: 2026-06-05 Owner: Core
Purpose: Draft for authentication, authorization, ACLs, rate limiting, captchas, and secure administrative flows.
- Defines security expectations for public and administrative routes.
- Covers roles, ACLs, rate limiting, captcha integration, secrets, and module-provided security extensions.
- Depends on core architecture, plugin modules, and editor workflows.
- Keeps security Symfony-native while leaving explicit replacement points for captcha and future authentication extensions.
Security should use Symfony Security as the primary model. Authentication, authorization, CSRF protection, voters, rate limiting, secrets, and route access rules should remain recognizable Symfony concepts.
The first security layer should protect administrative workflows, publishing actions, imports, module management, backup/restore, updates, and API access. Public routes should stay minimal and explicit. Module-provided routes and editor actions must declare permissions before they become usable.
The first admin authentication flow should use Symfony's normal login and password hashing. The initial admin user is created during setup, and bin/setup --reset-password should support password reset for recovery edge cases. APP_SECRET should be generated during setup and stored in .env.local.php.
Schema administration is security-sensitive. Editing database-backed schema Twig, changing content-query/list fields, publishing schema changes, and altering language or generic variant availability should require explicit permissions and audit logging.
Captcha should be modeled as an extension point first. The core should define the CaptchaProvider contract and resolver, while concrete IconCaptcha behavior is tracked in a separate feature draft before implementation.
Abuse handling should be progressive rather than relying on one coarse IP threshold. The system should be able to allow normal traffic, throttle suspicious bursts, require captcha for configured workflows, temporarily block high-risk behavior, and hard-block only clear hostile signals or explicit deny policies.
Captcha should use a global form field integration. When a workflow includes the captcha field, the field asks the configured provider resolver to render and validate the active provider. If no captcha module is installed or the selected provider is none, the field validates successfully so workflows do not break just because captcha is disabled or unavailable.
- Use
config/packages/security.yamlas the source of route/firewall/access control configuration. - Use
/user/loginas the public browser login route and let Symfony form login handle credential checks againstUserAccount. - Keep first user-account routes under
/user/*: a/useraccount entry point, public login and reset-password entry points, config-gated registration, authenticated profile/password/API-key/invitation surfaces, and later invitation acceptance when onboarding ships. - Use durable account tokens for invitation setup, registration setup, password reset, and password-change security review links. Tokens store type, status, target email/user, target role, group identifiers, creation time, expiry time, consumption time, and safe metadata.
- Generate mail-bound action URLs through the shared absolute URI generator from either an already absolute URL or the configured
site.urlvalue before passing them to the mail delivery boundary. - Normalize email addresses to lowercase on write and mail delivery, and use case-insensitive account lookup for recovery/registration/invitation flows so mixed-case legacy rows remain recoverable.
- Require ACL group identifiers to be lowercase snake_case with at least 3 characters everywhere they are accepted or referenced.
- Keep invitation and registration account links configurable with a 24-hour default TTL for newly issued links; keep password-reset links on a fixed one-hour TTL.
- Let administrators reissue pending invitation, registration, and password-reset links by rotating the token hash and stored expiry. Used, revoked, and pending-approval tokens are not reissued.
- Revoke older pending invitation/registration/reset tokens for the same target when a newer equivalent link is issued.
- Keep public registration and password-reset request screens enumeration-safe: the UI returns the same success state whether an account exists or not. If a registration address already belongs to a user, emit a dedicated existing-account mail flow with the username for the future mailer instead of showing it in the UI.
- Use a mailer-shaped account-link delivery boundary until the real mailer exists. The current implementation writes DEBUG payloads to the message log with deterministic mail flow keys, locale, available template parameters, and clear action URLs for local testing.
- Treat one-time-token action URLs in the message-log delivery stub as an intentional development/debug path until the mailer replaces or gates that implementation. Plain tokens must not be logged as a separate context field.
- Provide user-facing API-key management under the user context. API keys are generated with user-chosen prefixes, default to read-only unless explicitly changed, run in the owning user's context, and require password re-authentication before encrypted key material can be revealed again.
- Store API-key lookup hashes and encrypted payloads with
APP_SECRET-derived material. Do not rotateAPP_SECRETduring normal maintenance; change it only as an emergency response to a confirmed or likely compromise. If the environment-specificAPP_SECRETfingerprint changes after initial baseline, revoke active API keys and issue password-reset links to active owners through the account-link delivery boundary as a failsafe for operators without direct CLI recovery access. - Let users close their own account through a dedicated confirmation page with password confirmation and explicit confirmation. Closure marks the account deleted, revokes adjacent credentials/recovery links, emits an account-closed mail flow with the configured retention window, logs the user out, and redirects to the homepage.
- Deleted accounts can be reactivated through registration or invitation links. The existing user UUID is reused for attribution continuity, the global role is set from the accepted token, and ACL groups are replaced with the token's still-existing groups instead of restoring previous permissions.
- Missing or purged user references should resolve through a central identity fallback that returns localized generic deleted-user display values.
- Use Symfony voters for content, module, editor, and operation permissions.
- Define exactly one global user role per account, with Public, User, Moderator, Author, Publisher, Curator, Manager, Director, Admin, and Owner mapped to numeric access levels
0through9and inherited Symfony roles. - Use CSRF protection for state-changing browser workflows.
- Use Symfony Rate Limiter for application-level throttling such as login attempts, contact forms, API usage, import attempts, and captcha failures.
- Use separate Rate Limiter buckets for different intents, such as login attempts, contact form posts, registration, guest comments, captcha refreshes, captcha failures, API usage, import attempts, and suspicious probes.
- Avoid using a single coarse per-IP limiter as the only abuse-control decision.
- Support progressive abuse handling such as allow, throttle, require captcha, temporary block, and hard block.
- Document that edge/server-level rate limiting is still needed for DoS protection.
- Keep secrets out of manifests, docs, logs, fixtures, and committed config.
- Generate
APP_SECRETduring setup and store it in.env.local.php. - Create the first admin user during setup.
- Provide
bin/setup --reset-passwordfor admin password recovery edge cases. - Provide account recovery and invitation/onboarding flows once multi-user administration ships.
- Track user login metadata such as last login time where useful for audit and administration.
- Define a
CaptchaProviderreplacement contract and resolver when captcha is implemented. - Define a global captcha form field that renders and validates the configured provider through the captcha resolver.
- Make captcha usage configurable per workflow, with contact forms, registration forms, and guest comments as the first expected protected workflows.
- Allow provider selection through configuration. With only the first-party module installed, the default choices should be
icon_captchaandnone. - Treat provider value
noneand missing provider modules as successful captcha validation so configured forms do not break. - Ensure captcha never replaces CSRF, ACL checks, authentication, or business validation.
- Use audit logging for high-impact actions such as publish, delete, import apply, module enable, migration run, backup restore, and update.
- Audit schema Twig changes, content-query/list field changes, variant availability changes, and localization routing changes.
- Use content ACL restrictions to limit public or authenticated content visibility to specific ACL groups where configured.
- Implement ACL foundations early and use them for admin access such as
/admin. - Model ACL groups as contextual memberships with stable identifiers, translatable names, and a single-digit minimum assignable role from 0 to 9. Groups do not determine the user's global role, may be empty, and may be edited or deleted unless configured as the registration default.
- Usernames must be 5 to 30 ASCII characters, start with a letter, and may contain only letters, digits, hyphens, and underscores. Self-service username changes are controlled by a default-off setting; invitation, registration, and reactivation setup forms still allow choosing a valid free username.
- Use backend area access levels before route-specific voters exist:
/editorrequires Author or higher, manager-only editor functions should require Manager or higher, and/adminrequires Admin or higher. - Allow users to belong to multiple ACL groups independently of their exact global role; anonymous visitors use Public access level
0. - Require every active or inactive user account to keep at least the User role, and ensure at least one active Owner account always remains available.
- Owners may manage all roles and groups. Admins may enter Admin but cannot manage peer Admin users, Owner users, or groups at or above their own role level.
- Filter Admin User invitation/edit group choices to groups the acting administrator may assign and the target role is allowed to hold.
- Require explicit impact review before ACL group updates and deletes. Confirmed changes run through the shared LiveLog operation overlay and remove deleted group identifiers through domain-owned ACL reference providers for known ACL-bearing records, including user memberships, pending account tokens, content items, schema versions, and site menu items.
- Allow entities and schema versions to override inherited ACL behavior with separate capability fields: min-level plus optional group identifiers. Content uses
view,edit, andmanage; schemas useuse,edit, andmanage. - Treat
NULLACL fields as inherit. Treat an empty group list as no group exception, so only the level rule applies. - Resolve inherited ACL rules through a shared resolver that evaluates the nearest explicit rule first, then falls back to capability defaults.
- Evaluate content ACL restrictions during public rendering, content-query/list field execution, API output, search results, and import/export previews.
- Require explicit permissions for editing or activating database-backed schema Twig.
- Require controlled services for content-query/list fields so editors cannot bypass permissions with arbitrary query logic.
- Require modules to declare permissions and route sensitivity before enabling admin routes or state-changing actions.
- Treat protected database-backed configuration values, such as the MaxMind API key, as administrator-only settings and never expose them through logs, diagnostics, public exports, or unprivileged UI.
- Ensure missing optional security configuration, such as a GeoIP provider key, degrades gracefully instead of producing hard runtime failures.
- Treat API authentication separately from browser authentication once the API draft is implemented.
- Functional-test access rules for public, admin, editor, module, and API routes.
- Test voters for allowed, denied, and missing-subject cases.
- Test CSRF protection on state-changing forms.
- Test rate-limited workflows with configured thresholds.
- Test separate rate-limit buckets do not block unrelated workflows.
- Test progressive abuse handling transitions where implemented.
- Test captcha provider selection and fallback behavior.
- Test the global captcha form field passes validation when provider is
noneor no captcha provider is available. - Test contact form, registration, and guest-comment workflow captcha configuration once those workflows exist.
- Test admin setup and
bin/setup --reset-passwordrecovery behavior once setup is implemented. - Test password reset, invitation/onboarding, and session-related account flows once multi-user administration ships.
- Test account-token expiry, reissue, duplicate-token revocation, and cleanup behavior.
- Test registration and password-reset request screens do not leak account existence through UI responses.
- Test password-change review links lock the account and notify administrators.
- Test self-service account closure revokes adjacent credentials and ends the session.
- Test deleted-account reactivation reuses the UUID while resetting ACL groups from the accepted token.
- Test APP_SECRET fingerprint rotation revokes active API keys and issues owner password-reset links.
- Test generated
APP_SECRETis written to.env.local.phpand not committed. - Test schema Twig editing and activation permissions.
- Test the shared ACL resolver for anonymous, level-based, group-based, inherited, and denied decisions.
- Test administrative hierarchy guardrails for editing users, assigning groups, creating groups, updating group minimum roles, deleting groups, self-lockout, last-owner protection, and role/group assignment boundaries.
- Test ACL group impact review and cleanup of deleted identifiers across all known ACL-bearing entities.
- Test content-query/list field permissions.
- Test ACL-restricted content visibility in public rendering, API output, content-query/list fields, and search results.
- Test protected configuration values are only visible/editable to authorized administrators.
- Test missing optional security provider configuration does not produce hard failures.
- Run
php bin/console lint:containerafter security service/configuration changes. - Review
config/packages/security.yamlfor every security-sensitive feature.
- Decision recorded: Use Symfony Security, voters, CSRF, and Rate Limiter as the foundation.
- Decision recorded: First admin authentication uses Symfony login and password hashing; setup creates the first admin user and
bin/setup --reset-passwordsupports recovery edge cases. - Decision recorded:
APP_SECRETis generated during setup and stored in.env.local.php. - Decision recorded: Captcha extensibility belongs in core through
CaptchaProviderand resolver contracts, while IconCaptcha behavior is tracked in its own feature draft before implementation. - Decision recorded: Captcha integrates through a global form field that uses the configured captcha provider resolver.
- Decision recorded: Captcha provider selection is configurable.
noneis a valid provider value and missing providers must validate successfully to avoid breaking workflows. - Decision recorded: Contact forms, registration forms, and guest comments are the first expected workflows to protect with captcha.
- Decision recorded: Abuse handling should be progressive and use workflow-specific limiters instead of relying on one coarse IP bucket.
- Decision recorded: Visitor identity should use a first-party, HMAC-protected, rotatable technical visitor cookie. Avoid machine IDs, advertising IDs, cross-site identifiers, and browser/device fingerprinting. When no valid visitor cookie is present, the request uses an
APP_SECRET-derived fallback visitor ID from source IP and normalized user-agent so cookie-disabled clients do not create a new unique visitor on every request. The response still receives a fresh random signed cookie token. The short-lived identity store keeps cookie hashes and fallback hashes separate: known valid cookie hashes win, then recent fallback hashes win, and newly issued cookies bind to the chosen visitor ID. This intentionally accepts temporary undercounting for same-IP/same-user-agent visitors so the system does not overcount every page view or first-cookie transition. - Decision recorded: Session hardening binds authenticated Symfony sessions to the first-party visitor signal. Login requests and legacy sessions without an existing binding initialize the binding; established sessions with a changed or missing visitor signal are treated as likely session duplication and terminated to prevent copied session cookies from staying usable.
- Decision recorded: Request IDs, visitor IDs, IP buckets, and related metadata may feed rate limiting and suspicious-behavior detection, but they are signals rather than sole proof of identity. Some enforcement may be IP-based and some visitor-based, depending on the abuse pattern.
- Decision recorded: Captcha does not replace CSRF, ACL, authentication, or business validation.
- Decision recorded: Symfony roles remain the primary authorization layer. ACL groups are an optional, separate project/domain permission layer with explicit AND/OR semantics for content trees, schemas, package domains, and similar granular access cases.
- Decision recorded: ACL group references use shared access-rule/reference ownership. Every ACL-bearing domain should expose impact and cleanup through a domain-owned provider so group review, cleanup, package usage, and entity/tree permissions do not rely on one ad-hoc cross-domain scanner.
- Decision recorded: Require module-declared permissions before module admin routes become active.
- Decision recorded: Split operational route surfaces into
admin/for system administration andeditor/for content authoring, review, publishing, and content-management workflows.admin/starts at Admin, whileeditor/starts at Author and should use ACL capability checks so authors, managers, and administrators can access only the authoring functions they are allowed to use. - Decision recorded: Backend area checks expose an
AccessRuleboundary so future admin exceptions can add configured ACL groups without changing controller flow. The first router slice keeps admin at the Admin role boundary and keeps Owner as the unrestricted site-owner role. - Decision recorded: Editing or activating database-backed schema Twig and content-query/list fields is security-sensitive and requires explicit permissions plus audit logging.
- Decision recorded: Content ACL restrictions may limit visibility to specific ACL groups and must be enforced consistently across rendering, API output, content-query/list fields, search, and import/export previews.
- Decision recorded: Implement ACL foundations early, including
/adminaccess protection and content-record ACL restrictions, to avoid later structural rewrites. - Decision recorded: Use level-plus-group ACL overrides instead of one opaque ACL JSON payload for the first database baseline. Content capabilities are
view,edit, andmanage; schema capabilities areuse,edit, andmanage. - Decision recorded: Shared ACL resolution uses normalized actors, explicit or inherited rules, and capability defaults. Granted decisions emit
DEBUGmessages; denied decisions emitWARNmessages so future log views can filter ACL behavior. - Decision recorded: Public content delivery distinguishes access denial from missing or unpublished content for debugging and future security flows. Missing content, unpublished/deleted content, and unavailable language/variant contexts return
404; ACL-denied content returns401; private content and reserved routes such as/system/...return403. Browser authentication renders the login form for anonymous401responses while preserving the401status; authenticated users who are still denied receive the configured custom error page. - Decision recorded: Maintenance mode is controlled by the environment-backed
APP_MAINTENANCEflag, not by a database config key. When enabled, public requests return503 Service Unavailableunless the authenticated user has at least the Admin role;/admin,/user/login, localized login paths, and static asset prefixes remain reachable so administrators can still sign in and operate the site. Later error handling should resolve/system/error-pages/503before falling back to the system theme's default maintenance page. - Decision recorded: Multi-user administration should include password recovery, invitations/onboarding, and audit-relevant account metadata once user management expands beyond the first admin account.
- Decision recorded: Protected database-backed configuration values such as the MaxMind API key require administrator-only access and must degrade optional features gracefully when missing.
- Decision recorded: API keys are user-owned credentials that execute in the owning user's role and permission context. Administrators may oversee or revoke keys; operational endpoints such as Scheduler/Cron require an admin-owned key where elevated behavior is needed.
- Decision recorded: Reversible secrets and protected payloads should use an
APP_SECRET-rooted, context-labeled, versioned protector rather than independent direct-secret helpers.APP_SECRETrotation is an emergency action and should keep revoking affected API keys plus recovery flows unless a tested re-encryption path exists. - Decision recorded: The account-link message-log delivery stub may remain available only as an explicit
APP_DEBUG/debug aid with strong administrative warning once real mail delivery exists. Production mail should use the real mailer path. Until then, the stub may expose action URLs for local delivery but must not duplicate the plain one-time token as a separate log context value. - Decision recorded: User accounts store
settingsJSON for user preferences andstatusfor active/inactive/deleted directly onuser_account. Current/last lifecycle metadata such as created, modified, last login, password changed, and status changed lives in reusablestate_markerrows so admin tables can filter and sort without bloating every domain table. Forlast_login,marker_byremainsNULLandmarker_valuestores the IPv4/IPv6 address for user-facing self-audit hints. Full history still belongs in later audit logs, not in these lookup markers. - Implemented baseline: Symfony Security now uses the
UserAccountentity provider and native form login at/user/login. The native backend controller checks setup/admin/editor area access through the shared ACL resolver, and unauthorized backend/security flows return401through the shared HTTP error renderer so anonymous users see the login form and authenticated denied users receive the resolved error page. - Implemented baseline: Native
/user/*account routes now include profile editing, password changes, password-reset request/completion, disabled/admin-approval/auto-approval registration, invitation/registration acceptance where users choose username and password, and user-scoped API-key generation, revocation, and password-confirmed reveal through reversible encrypted storage. Mail-bound flows currently use an account-link delivery boundary that writes mailer-shaped DEBUG payloads with stable template keys, locales, available parameter keys, and setup/reset action URLs to the message log until a real mailer delivery implementation is available. - Implemented baseline: Admin user management now lists, searches, filters, sorts, and paginates user accounts; issues invitations instead of directly creating completed accounts; approves registration requests; changes account status and ACL group membership; creates password-reset links; shows user state-marker history with a filtered Audit Log link; and manages editable ACL groups with impact review before broad updates or deletions.
- Implemented hardening: Invitation and registration tokens now use the stored
expiresAttimestamp with a configurable 24-hour default for newly created links, password-reset tokens use a fixed one-hour TTL, the admin settings UI warns that TTL changes do not mutate already issued tokens, andaccount-tokens:cleanupremoves expired account-token rows for future scheduler integration. - Implemented hardening: Mail-bound account deliveries now generate absolute
action_urlvalues through the shared absolute URI generator, generic delivery errors reach the UI when link creation fails, email addresses normalize to lowercase on user/token/mail boundaries, password recovery uses a case-insensitive user-account lookup for mixed-case stored rows, and ACL group identifiers require lowercase snake_case with at least 3 characters consistently across groups, account tokens, access rules, and ACL-bearing content/menu metadata. - Implemented hardening: Admins can reissue pending account links, which rotates the token hash and stored expiry before writing a new link to the message log. New invitation, registration, and password-reset requests revoke older pending tokens for the same target so stale links stop working. Public registration remains enumeration-safe: existing account emails receive the same UI success state, while the message-log delivery boundary emits
account.registration.existing_accountwith the username for the future mailer. - Implemented hardening: Authenticated password changes and password-reset completions now issue a password-change notification flow with a one-time
security_reviewtoken. Following the review link disables the account withinactivestatus, consumes the token, audits the dispute, and emitsaccount.password_change.disputedfor administrator review. - Implemented foundation: Visitor identity now uses a first-party technical
system_visitorcookie with random signed cookie tokens, 30-day lifetime, HMAC validation, a file-backed cookie/fallback alias store, and an APP_SECRET-derived IP/user-agent fallback ID only when no known valid cookie is available. Stored/logged visitor IDs are compact 128-bit values derived withAPP_SECRET; raw visitor tokens, cookie hashes, and fallback hashes are not stored in access statistics. IP and proxy signals stay separate from the visitor bucket so future rate limiting and blocking can distinguish shared networks from individual browser/device visitors. - Decision recorded: Core cookies are limited to technical first-party values such as visitor identification, session/security binding, language preference, and appearance preference. Advertising or external analytics packages must bring their own isolated cookie strategy and consent flow instead of reusing core technical cookies. A future
ConsentProvider/cookie-policy registry can let packages declare cookie purposes, retention, vendor scope, and required consent categories while the core consent UI/enforcement layer remains centralized. - Implemented hardening: Successful logins bind the current Symfony session to the current visitor ID. Authenticated requests with no previous binding initialize the binding for compatibility with already active sessions; authenticated requests where an established binding no longer matches clear the security token, invalidate the session, redirect to login, and audit
auth.session_visitor_mismatch_terminatedwith the previous/current visitor IDs. - Decision deferred: A future "keep me logged in" checkbox should use Symfony's remember-me foundation with a separate server-side persistent login-token model instead of a bare identity cookie. The browser cookie should contain only an opaque selector/token, while the server stores the hashed token, owning user, expiry, current visitor binding, and revocation state. The remember-me trust window should be 7 days; automatic logins may rotate the token value but should keep the original expiry instead of silently sliding the window. An explicit credential login with the checkbox set can issue a new 7-day token. Successful auto-login creates a fresh Symfony session, binds it to the current
system_visitorcookie, revokes the token on manual logout and password/status/security events, and audits mismatches or reuse attempts. Backend-calculated signals such as IP buckets, user-agent family, client hints, request cadence, and concurrent use may raise risk, require step-up, or help investigation, but they must not be treated as sole hard identity proof because they are too noisy across legitimate networks and browser/device changes. Normal browser session lifetime may be reduced later, with 60 minutes as an initial candidate, but the final TTL belongs to the dedicated Security hardening slice. - Decision deferred: Privacy copy, richer false-positive recovery, re-authentication step-up behavior, and rate-limit policy belong to the dedicated Security hardening slice. Visitor ID remains suitable for statistics and risk signals, and is used as a hard factor only for detecting copied/duplicated authenticated sessions.
- Implemented hardening: Admin User Reviews now provides a compact queue for registration approvals, open or expired invitation/registration links, and password-change disputes while deliberately excluding password-reset tokens. Review actions approve/reject registration requests, resend/delete open account links, and reactivate disputed accounts by setting a random password and notifying the user to use password reset.
- Implemented hardening: Account status changes now pass through a lifecycle service. When an account becomes
inactiveordeleted, all non-revoked API keys and pending password-reset/security-review tokens are revoked so non-usable accounts do not retain adjacent credentials or recovery links. Users can close their own account from a dedicated confirmation page with password plus explicit confirmation; the flow marks the account deleted, revokes adjacent credentials, logs the user out, redirects to the homepage, and emitsaccount.closedwithretention_daysfor the future mailer. Deleted accounts are hidden from the main Admin User list and retained in a separate deleted-users view until the configurableuser.deleted_user_retention_dayswindow has passed; admins can activate or deactivate retained deleted accounts without requiring ACL group repair. - Implemented hardening: Deleted accounts can be reactivated through registration or invitation account links. The flow keeps the existing UUID for future content/comment attribution, applies the role stored on the accepted token, ignores token groups that were deleted meanwhile, and replaces previous ACL memberships with the token's remaining groups so old administrative permissions are not restored implicitly. Token acceptance also rechecks that the token email was not claimed by another account before persisting. User-reference rendering has a central
UserIdentityResolverfallback that returns a generic deleted-user identity when a stored UUID no longer resolves to a persisted account. A reusableDeletedUserCleanupservice powers the admin cleanup action, reassigns revoked stale API keys to the stable deleted-user account for later API-audit references, and is ready for the future global stale-cleanup scheduler task. - Implemented hardening: Usernames must be 5 to 30 ASCII characters, start with a letter, and may contain only letters, digits, hyphens, and underscores. The default-off
user.username_change.enabledsetting controls whether existing users may change their username from the profile screen; invitation and reactivation account setup flows still require choosing a valid free username. Missing or purged user references render through the localized deleted-user identity fallback instead of using a persisted username. - Implemented hardening: Admin user and ACL-group changes now enforce role hierarchy guardrails. Owners can manage all users, roles, and groups; Admins can enter Admin but cannot edit peer Admins or Owners, cannot self-demote below Admin while active, and invitation/edit forms validate both token target roles and assignable groups against the actor. Public registration may proceed without a default ACL group. The guardrails also prevent leaving the installation without at least one active Owner. ACL group updates and deletes require an impact review before confirmation, then run through the shared LiveLog operation overlay like package lifecycle actions; group deletion removes the identifier through domain-owned reference providers for user memberships, pending account tokens, content item ACL fields, schema version ACL fields, and site menu item ACL fields.
- Implemented hardening:
AppSecretRotationGuardstores an environment-specific APP_SECRET fingerprint after setup. A later fingerprint mismatch is treated as emergency compromise handling, not routine rotation: it revokes active API keys, creates one-hour password-reset links for active owners as a fallback whenbin/setup --reset-passwordis unavailable, then records the new fingerprint and audits the handling. - Implemented hardening: APP_SECRET rotation handling is idempotent per environment fingerprint for the current synchronous recovery flow. Once a mismatch is handled, the guard stores the new fingerprint even when owner reset URLs could not be generated for every owner, and the audit context records owner and issued-link counts so operators can see partial recovery. If any owner recovery submission fails, the guard persists normal owner-bound one-time reset tokens, writes an emergency recovery file under
var/recovery/{APP_ENV}/, and logs a warning withbin/setup --reset-passwordas manual recovery command, the recovery file path, and afailed_submissionslist containing the affected owner uid, username, email, and failure reason. The emergency file is outsidepublic/, uses restrictive permissions where supported, and contains reset paths for the affected owners only, not a universal account token. If future asynchronous mail delivery introduces new failure windows, a dedicated attempt journal may still be added in the Mailer/Security hardening slice. - Implemented baseline:
bin/setup --reset-passwordcan reset a user's password after displaying the matching account and confirming the operation. Interactive setup prompts now require repeated admin password entry to reduce CLI typos.