Security hardening
Authentication
Section titled “Authentication”Built on ASP.NET Core Identity.
| Control | Default |
|---|---|
| Password policy | At least 8 characters, with upper case, lower case, and a digit |
| Unique email | Enforced |
| Lockout | 5 failed attempts, 15 minutes (AccountSecurity:MaxFailedAccessAttempts, LockoutMinutes) |
| Two-factor | Authenticator app with recovery codes |
| Session cookie | Secure, HttpOnly, SameSite=Lax |
Two-factor authentication is available to every user but is not enforced centrally. The security health report lists accounts without it - review it rather than assuming administrators enabled it. See Reports.
Self-registration
Section titled “Self-registration”Disabled by default, and it should stay that way.
Rate limiting and shedding
Section titled “Rate limiting and shedding”Two independent mechanisms, addressing different failure modes.
Rate limiting applies chained fixed windows. Authentication forms use two buckets at once: a strict per-account allowance that applies across all source addresses, and a looser per-address allowance sized for shared office egress.
The two-bucket design exists because a single per-address bucket means one user failing to sign in repeatedly locks out everyone behind the same public address - an entire office, because of one person’s forgotten password.
Static assets and the health endpoint are exempt, so a browser loading a page does not consume a
user’s allowance. Rejections return 429 with Retry-After.
Authentication concurrency bounds simultaneous password verification, which is deliberately
CPU-expensive. Its permit limit defaults to the processor count and its queue to four times that.
Over that, requests are shed with 503 and Retry-After rather than queueing until the connection
pool is exhausted - failing a few requests fast instead of degrading everything slowly.
Content Security Policy
Section titled “Content Security Policy”Enforced with a per-request nonce. Inline scripts without the correct nonce are blocked, so an
injected <script> does not execute even if it reaches the page.
Security:Csp:Mode switches between enforcing and reporting. Reporting mode is for validating a
policy change; it is not a deployment state. Security:Csp:ReportUri collects violation reports.
Transport
Section titled “Transport”Security:Hsts controls max-age (365 days by default), subdomain inclusion, and preload.
Preload is effectively irreversible on the timescale of a browser release cycle. Enable it only when every subdomain will serve HTTPS indefinitely.
Security:ReferrerPolicy defaults to strict-origin-when-cross-origin.
Security:PermissionsPolicy disables camera, microphone, geolocation, and interest-cohort by
default - the application needs none of them.
Forwarded headers
Section titled “Forwarded headers”Security:ForwardedHeaders is fail-closed. With no KnownProxies or KnownNetworks
configured, forwarded headers are not trusted at all.
That is the correct default: a client that can set X-Forwarded-For freely can present a new
address per request and defeat address-based rate limiting entirely. The cost of leaving it
unconfigured behind a real proxy is that every request appears to come from the proxy.
Configure it deliberately, and set ForwardLimit to the actual number of proxies in the chain.
Data protection
Section titled “Data protection”The key ring protects every authentication cookie, the “remember this machine” two-factor cookie, and stored secrets such as the antivirus API key.
| Setting | Requirement |
|---|---|
DataProtection:KeyPath |
Required outside Development. Absolute, and outside the content root. The host refuses to start otherwise. |
DataProtection:CertificateThumbprint |
X.509 protection. Required for non-Windows hosts, multi-instance deployments, and restorable backups. |
Without a thumbprint the ring is DPAPI-protected, which is machine-scoped and cannot be restored onto different hardware. See Backup.
Database least privilege
Section titled “Database least privilege”The application runs as a member of DocsentryAppRole, distinct from the schema owner. It cannot
create databases, alter schema, or write schema versions. A startup probe verifies this and
refuses to run over-privileged.
Governance surfaces - the audit log, workflow action history - are append-only for this role. The application can add to them but cannot update or delete. This is a database grant, not an application rule, so a defect or compromise in the application cannot rewrite the audit trail.
Uploads
Section titled “Uploads”Uploads are allowlisted, and extension, declared MIME type, and sniffed content must all agree. Antivirus scanning is fail-closed: content that has not been cleared is not served, previews included.
Application surface
Section titled “Application surface”- SQL is parameterised throughout; there is no string concatenation with user input.
- API errors are returned as problem details, with a defined taxonomy rather than leaked exception text.
- API DTOs are sealed records. Database row models and Identity entities are never exposed.
- Search results, the recycle bin, and reports are permission-trimmed - a document you cannot access does not appear as an inaccessible result; it does not appear.
Known limitations
Section titled “Known limitations”Stated plainly, because a security page that claims completeness is not useful:
- Self-registration auto-confirms, as described above. It is closed rather than repaired.
- Two-factor is not centrally enforceable - it is available to users, not mandated.
- Documents and the database are not encrypted at rest by the application. Use storage-level or database-level encryption if that is required.
- Audit immutability rests on the database grant, which is strong, but there is no hash chain or off-host sink. Someone with schema-owner access to the database could still alter history.
Related
Section titled “Related”- Deployment - the pre-flight checklist.
- Permission model - authorisation, as distinct from authentication.
- Reports - the security health report.