Skip to content

Security hardening

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.

Disabled by default, and it should stay that way.

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.

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.

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.

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.

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.

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 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.

  • 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.

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.