Auth & Security Hardening
The defaults are safe to deploy, but production APIs benefit from a few extra layers. This page collects the practical checklist.
Authentication
- Use
auth.JWTAuthwith an asymmetric algorithm (RS256/ES256) when tokens are issued by an external provider. SymmetricHS256works when the signing service and the API share infrastructure. - Use
auth.JWKSAuth(jwksURL, opts…)when the issuer publishes a rotating JWK Set (/.well-known/jwks.json). It fetches and caches the keys, selects the signing key by the token’skid, and refetches on an unknownkidso key rotation needs no redeploy. AllJWTOptions(Issuer, Audience, claim mappings, ClockSkew) apply. Prefer this over pinning a single staticPublicKeyagainst an issuer that rotates. RSA (RS256/384/512) and EC (ES256/384/512) supported. - Set
JWTOptions.IssuerandAudienceso tokens issued for another audience are rejected. - Set
JWTOptions.TenantClaimfor multi-tenant APIs — the verified value ends up onctx.Auth.TenantIDand feedsdb.Tenancy. - Never accept anonymous writes by default. Register
auth.JWTAuth(orauth.APIKeyAuth) on the Auth step scoped toOpCreate,OpUpdate,OpDelete. Useauth.AllowPublicReadwhen reads are truly public.
server.Pipeline.Auth.Register(auth.AllowPublicRead())
server.Pipeline.Auth.Register(auth.JWTAuth(secret, auth.JWTOptions{
Issuer: "https://accounts.example.com",
Audience: "https://api.example.com",
TenantClaim: "org_id",
}), maniflex.ForOperation(maniflex.OpCreate, maniflex.OpUpdate, maniflex.OpDelete))
Verifying tokens from an issuer that rotates its signing keys via JWKS:
server.Pipeline.Auth.Register(auth.JWKSAuth(
"https://accounts.example.com/.well-known/jwks.json",
auth.JWTOptions{
Issuer: "https://accounts.example.com",
Audience: "https://api.example.com",
TenantClaim: "org_id",
}))
Authorisation
- Gate sensitive operations with
auth.RequireRole. Don’t rely on the UI to hide them. - Use
db.Tenancyordb.ForceFilterfor row-level scoping. These run on the DB step so they apply to lists, reads, and writes uniformly — UI code cannot accidentally bypass them. - Strip privileged values with
validate.ForbiddenValuesfor role fields and similar — prevent a normal user from promoting themselves by including"role": "admin"in a payload.
Secrets and PII
- Hash passwords with
service.HashField, never store them raw. - Use
writeonlyon credential fields so they are accepted on input but never returned in responses. - Encrypt sensitive columns with
mfx:"encrypted"and a configuredKeyProvider. Pair with thekey:sub-option for per-domain keys. - Redact in responses with
response.RedactFieldwhen a column is visible to some callers and hidden from others. - Gate writes with
validate.FieldRolewhen a column is writable by some callers and not others (readonlyis all-or-nothing). Without it, a privileged field needs its own endpoint to keep it off the general PATCH.
Input
- Set
Config.QueryTimeoutso a slow query can’t tie up a connection indefinitely. - Cap body sizes with
body.MaxBodySizewhere you know the upper bound. The default 4 MB limit catches accidents, but a 10 KB endpoint should enforce 10 KB. - Strip unknown fields with
body.StripUnknownFieldsin environments where you want a strict contract — every accepted field appears on the model. - Validate beyond tags with
validate.RegexField,validate.UniqueField, andvalidate.CrossFieldValidate. The built-inmfx:rules cover the common cases; everything else belongs in middleware.
Output
- Set security headers globally via
response.AddHeader:Strict-Transport-Security,X-Content-Type-Options,Referrer-Policy. - List CORS origins explicitly with
response.CORSHeaders(origins...)— origins are required (there is no permissive wildcard default; it panics if you pass none), and"*"cannot be combined with credentials. - Cap rate-sensitive endpoints with
db.RateLimitso password resets and similar can’t be brute-forced.
Transport
- Terminate TLS at the load balancer or reverse proxy, not in the maniflex process. The framework is HTTP/1.1 + HTTP/2 ready.
- Set
Config.TrustProxyHeaders: trueonly when behind a trusted proxy. It is off by default: the client IP is the direct TCP peer, so a caller cannot forge it. When on, the IP is read fromX-Forwarded-For/X-Real-IP, which is only safe if the proxy overwrites (not appends) any inbound value the client sent. Every IP-keyed feature —db.RateLimit, idempotency scoping, and read-audit records — depends on this being correct; leaving it off while directly internet-facing keeps per-IP limits and audit logs honest. - Set
Config.PathPrefixto a non-default value if the proxy mounts the API at a custom path. Don’t rewrite paths inside the application.
Operations
- Use a JSON-emitting
sloghandler in production so logs are structured and ingestable by your aggregator. - Set
Config.ServiceName— every log line and audit record carries it. - Enable
Config.HealthCheckDBfor Kubernetes readiness probes; tuneConfig.HealthTimeoutshorter than the probe timeout. - Use
Config.PanicLoggerto route panics to a different sink than the rest of the framework logs, so they are easier to alert on.
Audit
- Register
db.AuditLogatmaniflex.Afterfor mutating operations. The records carry actor, model, operation, and a diff of the affected row. - Use
maniflex.ModelConfig{Versioned: true}on sensitive models. Every change writes a row to a sibling{model}_historytable.
Checklist
A reasonable production stack:
// Auth
server.Pipeline.Auth.Register(auth.JWTAuth(secret, jwtOpts))
server.Pipeline.Auth.Register(auth.RequireRole("admin"),
maniflex.ForModel("User"), maniflex.ForOperation(maniflex.OpDelete))
// Body
server.Pipeline.Deserialize.Register(body.MaxBodySize(32<<10),
maniflex.ForModel("PasswordReset"))
server.Pipeline.Validate.Register(body.StripUnknownFields())
// DB
server.Pipeline.DB.Register(db.Tenancy("org_id", tenantFromAuth))
server.Pipeline.DB.Register(db.RateLimit(db.RateLimitConfig{
RequestsPerMinute: 10,
Key: keyByIP,
}), maniflex.ForModel("PasswordReset"))
server.Pipeline.DB.Register(db.AuditLog(auditSink),
maniflex.ForOperation(maniflex.OpCreate, maniflex.OpUpdate, maniflex.OpDelete),
maniflex.AtPosition(maniflex.After))
// Response
server.Pipeline.Response.Register(response.CORSHeaders("https://app.example.com"))
server.Pipeline.Response.Register(
response.AddHeader("Strict-Transport-Security", "max-age=63072000"))
server.Pipeline.Response.Register(response.Logging(slog.Default()),
maniflex.AtPosition(maniflex.After))