Hinatadocs

Security model

Hinata is built to run on the public internet. This page lists the security controls, their environment variables and an operator checklist, all mapped to the OWASP Top 10.

Registration, 2FA and sessions from the user's side: Authentication. Federated login: Single sign-on.

Tokens and passwords

  • Stateless JWT, HS512. Access tokens are short-lived, and a separate refresh token issues new ones. The refresh token is rejected for normal API access and only works at the refresh endpoint. A stolen access token expires quickly, and a stolen refresh token can't read data.
  • Revocable sessions. Each token carries a session id (sid) tied to a record in the sessions collection. You can revoke single sessions without rotating the signing secret. See Authentication → Sessions.
  • BCrypt strength 12 for password hashing, with a 10-character minimum length. Length and a deliberately slow hash protect against brute force.

Change the JWT secret before you expose the server

HINATA_JWT_SECRET is the HS512 signing key and must be a real secret of at least 64 characters in production. Generate one with: bash openssl rand -base64 64 | tr -d '\n' Anyone who knows this secret can forge tokens for any user. Never use the default.

Login lockout and rate limiting

Two independent layers protect logins and the API.

Database-backed login blocking. Failed logins are counted, and the account or identifier is locked after a threshold. The counter lives in MongoDB, so the block survives restarts and works across multiple server instances.

VariableDefaultPurpose
HINATA_MAX_LOGIN_FAILURES5Failed attempts before the identifier is blocked
HINATA_LOGIN_BLOCK_MINUTES15How long the block lasts

Per-IP rate limiting (via bucket4j) caps requests per client IP. /auth/** gets a stricter budget against password spraying and account enumeration.

VariableDefaultPurpose
HINATA_RATE_LIMIT_ENABLEDtrueMaster switch for rate limiting
HINATA_RATE_LIMIT_API300Requests per minute for general API
HINATA_RATE_LIMIT_AUTH10Requests per minute for /auth/** (strict. The public SSO provider lookup counts against the API budget)

Rate limiting needs the real client IP

Behind a reverse proxy, every request otherwise seems to come from the proxy. Set HINATA_TRUSTED_PROXIES to the CIDR(s) of your load balancer or proxy, and X-Forwarded-For is only honoured from them. If it is empty, Hinata trusts no forwarded header. That is safe, but every client looks like the proxy. See Reverse proxy & TLS.

Authorization

  • Role-gated admin surface. Every route under /api/v1/admin/** requires the ADMIN role. A normal token can't reach admin functions.
  • Tenant and project visibility. Team membership decides project visibility across the app: a user only sees projects their team grants (see Projects & teams).
  • No back door for admins. The ADMIN role only opens the admin area. An admin sees projects, teams, issues, boards, knowledge base pages, search hits and time entries only through their own membership, like everyone else.
  • Organization duties have their own role. The settings for time tracking, approvals, absences, holidays and billing need the ORG_ADMIN role. Everyone else gets a 403 there, admins included. See Organization.
  • Roles you cannot give yourself. An admin cannot make themselves an organization admin, another admin has to do it. This is a transparency measure, not a hard barrier, because a second admin account would still get around it. That is why every grant is recorded, and that cannot be switched off. The last organization admin cannot be removed, deactivated or deleted. Every organization admin is told when someone joins or leaves the role.
  • No account takeover by admins. When an admin changes a person's sign-in address, the person gets a message at their old address. For one day after that, no password reset reaches them, neither from the admin area nor from the public "forgot password" page, which quietly sends nothing during that time.
  • A separate audit log. Admin area → Audit only shows platform records: sign-ins, accounts, configuration and integrations. Records about working time, timesheets and absences are in the log on the Organization page. Records about absences are only shown there to whoever keeps absences. Organization admins decide which of these events are recorded. Admins cannot change that, and the platform's master switch does not turn them off. Records about issues and pages appear for admins without their details, so without issue keys and page ids. Changes to the admin and organization admin roles, and any change of a sign-in address by an admin, are always recorded and cannot be switched off.
  • Knowledge base protected by roles. A page can be read by whoever sees its project, by the Team-Admins of its team, or by members the team opened it to. Pages with neither project nor team are read only by their author. Search only returns what the caller can reach. Only someone with authority over a page's current place can move it somewhere else, and only its author can make it private.
  • Public endpoints are explicit. Only this short allowlist works without a token: /meta, /setup/status, /setup, /auth/login, /auth/refresh, /auth/sso/providers, /actuator/health. Everything else needs a Bearer token.

Hardened HTTP responses

  • Security headers on every response, including HSTS (forces HTTPS), a strict Content-Security-Policy and Referrer-Policy: no-referrer.
  • Stable, localized JSON errors with no stack traces. The server resolves errors from message bundles based on the client's Accept-Language and always returns the same shape. Internal paths, class names and stack traces never reach clients.
  • Regex-escaped search input. Search terms are escaped before they reach the query layer, so a crafted term can't become an injected or expensive regular expression.

File uploads and object storage

  • Content type and size are validated on upload, so clients can't slip in disallowed or oversized files (limits are ENV-driven).
  • Randomized S3 object keys. Stored objects can't be guessed or enumerated by name.
  • Presigned downloads. Attachments are served through short-lived presigned URLs instead of a public bucket, so access is scoped and time-limited.

Encryption at rest for integration secrets

Git access tokens and other integration secrets are encrypted with AES-GCM before they reach the database, using the key in HINATA_GIT_TOKEN_SECRET. Secrets are write-only in the admin API and never returned. Change the default key in production. Rotating it re-encrypts stored tokens.

OWASP Top 10 mapping

OWASP Top 10 (2021)How Hinata addresses it
A01 Broken Access ControlADMIN-gated admin routes, ORG_ADMIN-gated organization routes, no content rights from platform roles, explicit public allowlist, team and project visibility, tokens revocable per session
A02 Cryptographic FailuresJWT HS512, BCrypt 12 passwords, AES-GCM encryption of integration secrets at rest, TLS everywhere (operator)
A03 InjectionRegex-escaped search, parameterized Mongo access, uploads validated for content type and size
A04 Insecure DesignRefresh tokens rejected for API use, write-only secrets, auth callbacks via deep link, authorization state stored in MongoDB
A05 Security MisconfigurationHardened headers (HSTS/CSP/no-referrer), API docs UI off by default in prod, trusted proxy allowlist, stable errors without stack traces
A06 Vulnerable ComponentsActively maintained Spring Boot 4 / Java 21 base. Keep images updated (operator)
A07 Identification & Auth FailuresPassword minimums, database-backed login lockout, strict /auth/** rate limiting, TOTP 2FA, revocable sessions
A08 Software & Data IntegrityGit webhooks with verified signatures, a commit ledger that applies each commit only once (see Git integration)
A09 Logging & Monitoring/actuator/health for probes. Errors are logged on the server without leaking internals to clients
A10 SSRFIntegrations run through the server with fixed provider endpoints instead of client-supplied URLs, and a logo set by URL is fetched only from public addresses on port 80 or 443, with every redirect checked again

Hardening checklist for operators

Do these before going live

  • Change HINATA_JWT_SECRET to a fresh 64-char secret (openssl rand -base64 64).
  • Change every default password: MONGO_ROOT_PASSWORD, MINIO_ROOT_PASSWORD and the TLS keystore and truststore passwords (HINATA_MONGO_TLS_*_PASSWORD, default changeit).
  • Change HINATA_GIT_TOKEN_SECRET so integration tokens are encrypted with your own key.

Then tighten the perimeter

  • TLS everywhere: terminate HTTPS at your reverse proxy and use TLS between services. Run MongoDB with X.509 client auth in production (see MongoDB & X.509).
  • Set HINATA_TRUSTED_PROXIES to your proxy's CIDR so rate limiting and lockout see the real client IP.
  • Disable the docs UI in prod: keep HINATA_DOCS_ENABLED=false so the Scalar API docs UI is not exposed.
  • Scope CORS: set HINATA_CORS_ALLOWED_ORIGINS to exactly your web app origin(s), nothing broader.
  • Don't trust a request because of where it comes from: when a logo is set by URL, the server downloads it itself. It refuses addresses on its own machine and in private networks, but it reaches every public address on port 80 or 443, including your own reverse proxy and internal services with a public IPv4 or IPv6 address. So put a login in front of every service that lets requests in only because of where they come from, such as your server's IP or your internal network.
  • Keep images updated: pull new ghcr.io/hinata-platform images regularly for security fixes. See Backups & upgrades.
  • Keep the server clock in sync (NTP). Token expiry and SAML SSO depend on it.

Where to go next