# Best Practices for Securing Your Logto Deployment: A Complete Production Guide

> Secure your Logto deployment with production best practices. Implement TLS, Helmet, RLS, secret rotation, and rate limiting for robust authentication.

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: best-practices
- Published: 2026-07-05

---

**Implement TLS termination, enable Helmet security headers, enforce PostgreSQL Row-Level Security, rotate secrets via environment variables, and apply rate limiting to authentication endpoints to harden your Logto deployment.**

Logto is an open-source OIDC provider and identity management platform that handles sensitive authentication data, user credentials, and tokens. Because it serves as the central identity layer for your applications, securing your Logto deployment is critical for protecting user data and preventing unauthorized access. This guide covers production-ready hardening techniques derived directly from the `logto-io/logto` source code, including specific file paths and configurations you can implement today.

## Terminate TLS at the Reverse Proxy

Always serve Logto over HTTPS by terminating TLS at a reverse proxy such as Nginx or a cloud load balancer before forwarding traffic to Logto services. This prevents network eavesdropping and man-in-the-middle attacks while keeping the application layer simple.

The official Docker Compose example shows the expected port exposure pattern, leaving HTTPS termination to the host infrastructure:

```yaml
version: '3.8'
services:
  logto:
    image: logto/logto:latest
    environment:
      - DB_URL=postgres://postgres:strongpassword@db:5432/logto?sslmode=require
    ports:
      - "3001:3001"   # user API

      - "3002:3002"   # admin API (firewall this port)

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: strongpassword

```

Configure your reverse proxy to forward only encrypted traffic and set the `X-Forwarded-Proto` header to `https` so Logto correctly generates absolute URLs in OIDC responses.

## Enforce Security Headers with Helmet Middleware

Logto uses Helmet middleware to set security headers that mitigate XSS, click-jacking, and other browser-based exploits. In [`packages/core/src/middleware/koa-security-headers.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/middleware/koa-security-headers.ts), the default configuration applies strict Content Security Policy (CSP), X-Frame-Options, and Referrer-Policy headers:

```typescript
// packages/core/src/middleware/koa-security-headers.ts
import helmet, { type HelmetOptions } from 'helmet';

export const defaultExperienceSecurityHeaderSettings: HelmetOptions = {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
    },
  },
  referrerPolicy: { policy: 'no-referrer' },
};

export const applySecurityHeaders = async (ctx: Koa.Context) => {
  await helmet(defaultExperienceSecurityHeaderSettings)(ctx.req, ctx.res);
};

```

As of version `3.13.0` noted in [`packages/core/CHANGELOG.md`](https://github.com/logto-io/logto/blob/main/packages/core/CHANGELOG.md), CSP enforcement is mandatory—the `reportOnly` flag was removed, making content injection protections active by default. Customize these settings by modifying the exported `HelmetOptions` before mounting the Koa application.

## Enable PostgreSQL Row-Level Security

Logto implements multi-tenancy with strict data isolation at the database level. Every tenant table is created with `ENABLE ROW LEVEL SECURITY` via the migration script in [`packages/schemas/tables/_after_each.sql`](https://github.com/logto-io/logto/blob/main/packages/schemas/tables/_after_each.sql):

```sql
-- packages/schemas/tables/_after_each.sql
ALTER TABLE ${name} ENABLE ROW LEVEL SECURITY;

```

This guarantees that a compromised tenant cannot read another tenant’s data. When the first migration runs (`pnpm alteration deploy`), PostgreSQL automatically enforces row-level policies that restrict rows to the current tenant ID. Ensure your database user does not have the `BYPASSRLS` attribute in production, and verify that migrations successfully apply the security clauses during deployment.

## Secure Secret Management and Rotation

Store all sensitive credentials—including API secrets, JWT signing keys, and database URLs—via environment variables rather than configuration files. Logto expects variables like `DB_URL` and `JWT_SECRET` to be injected at runtime, as documented in [`packages/cli/README.md`](https://github.com/logto-io/logto/blob/main/packages/cli/README.md) and implemented throughout the core packages.

Create a `.env` file based on the repository's `.env.example` that excludes real values from version control:

- Rotate `JWT_SECRET` regularly and restart services to apply new keys
- Use distinct credentials for the admin API versus the user-facing API
- Enable `sslmode=require` in your PostgreSQL connection string to enforce encrypted database connections

Never commit production secrets to source control, and use your cloud provider's secret management system (AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault) to inject these values into container environments.

## Separate Admin and User Ports

Logto runs user-facing APIs and administrative APIs on distinct ports—`3001` for user APIs and `3002` for admin APIs. This architectural separation allows you to apply stricter network controls to privileged endpoints:

- Expose port `3001` to public load balancers for authentication flows
- Restrict port `3002` to internal networks or VPN access only
- Apply additional authentication layers or IP whitelisting to the admin port at the reverse proxy level

## Implement Rate Limiting and MFA

Protect against credential-stuffing and brute-force attacks by leveraging the rate-limiting definitions in [`packages/schemas/src/consts/message-rate-limit.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/consts/message-rate-limit.ts):

```typescript
// packages/schemas/src/consts/message-rate-limit.ts
export const verificationRateLimit = {
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // allow 5 failed attempts per window
};

```

Configure your reverse proxy or API gateway to enforce these limits on verification endpoints. Additionally, enforce Multi-Factor Authentication (MFA) for admin accounts through the account-center UI available at `/account/security`, which exposes MFA configuration options in the console.

## Disable Unused Features and Adopt OAuth2 Standards

Reduce your attack surface by disabling features you do not use. Many capabilities (such as SSO or advanced MFA) are gated behind the `isDevFeaturesEnabled` flag in [`packages/core/src/feature-flags.ts`](https://github.com/logto-io/logto/blob/main/packages/core/src/feature-flags.ts). Keep these disabled in production unless specifically required.

For API security, Logto has transitioned to standard OAuth2 bearer token schemes as noted in the `2.0.43` changelog entry in [`packages/core/CHANGELOG.md`](https://github.com/logto-io/logto/blob/main/packages/core/CHANGELOG.md). Ensure your OpenAPI definitions use this security schema to improve interoperability with API gateways and security scanners.

## Summary

- **Use TLS termination** at your reverse proxy to encrypt all traffic before it reaches Logto services.
- **Apply Helmet security headers** via [`koa-security-headers.ts`](https://github.com/logto-io/logto/blob/main/koa-security-headers.ts) to prevent XSS and click-jacking with mandatory CSP enforcement.
- **Enable Row-Level Security** in PostgreSQL through the [`_after_each.sql`](https://github.com/logto-io/logto/blob/main/_after_each.sql) migration to isolate tenant data at the database level.
- **Inject secrets via environment variables** and rotate `JWT_SECRET` and database credentials regularly.
- **Separate admin and user ports** (`3001` vs `3002`) and firewall the admin interface.
- **Configure rate limiting** using the constants in [`message-rate-limit.ts`](https://github.com/logto-io/logto/blob/main/message-rate-limit.ts) and enforce MFA for privileged accounts.
- **Disable development features** via feature flags and use OAuth2 security schemas for API access.

## Frequently Asked Questions

### Should I use Logto's built-in HTTPS support or a reverse proxy?

**Use a reverse proxy.** The Logto source code is designed to run behind a TLS-terminating load balancer or reverse proxy (such as Nginx). The [`docker-compose.yml`](https://github.com/logto-io/logto/blob/main/docker-compose.yml) example exposes plain HTTP ports expecting the host to handle encryption. This approach centralizes certificate management and allows you to handle complex TLS configurations (like client certificates or specific cipher suites) at the edge.

### How does Logto prevent cross-tenant data leaks?

**Row-Level Security (RLS) in PostgreSQL.** Every tenant table created during migration includes `ENABLE ROW LEVEL SECURITY` as defined in [`packages/schemas/tables/_after_each.sql`](https://github.com/logto-io/logto/blob/main/packages/schemas/tables/_after_each.sql). The runtime automatically applies tenant ID filters to queries, ensuring that users from one tenant cannot access data belonging to another tenant, even if application-level checks fail.

### What is the recommended way to rotate JWT signing keys?

**Update the `JWT_SECRET` environment variable and restart.** Logto reads signing keys from environment variables at startup (see [`packages/cli/README.md`](https://github.com/logto-io/logto/blob/main/packages/cli/README.md) for documentation). To rotate keys, generate a new secret, update your environment configuration in your secret management system, and perform a rolling restart of your Logto containers. The new keys take effect immediately for newly issued tokens.

### Why are admin and user APIs on different ports?

**For network segmentation and access control.** Separating the admin API (port `3002`) from the user-facing API (port `3001`) allows you to apply stricter firewall rules to administrative endpoints. You can expose port `3001` publicly while restricting port `3002` to internal networks or VPN connections, minimizing the attack surface for privileged operations like tenant management and configuration changes.