# Security

> Authentication, authorization, transport encryption and encryption at rest in the Iggy server.

Rendered page: https://iggy.apache.org/docs/server/security/

Source: https://github.com/apache/iggy-website/blob/main/content/docs/server/security.mdx

Docs version: server 0.9.0, Rust SDK 0.11.0, CLI 0.14.0, Node.js SDK 0.10.0, and the Python, Java, Go and C# SDKs at 0.9.0

Iggy provides multiple layers of security covering authentication, authorization, transport encryption, and data encryption at rest.

## Authentication

Iggy supports two authentication mechanisms:

### Username and password

Users authenticate with a username and password, for example via the Rust SDK's `login_user()`. Passwords are hashed using **Argon2id** (a memory-hard hashing algorithm). On first single-node startup, the server generates a random password for the initial root account (username `iggy` by default) and writes it to the logs. A first cluster boot requires explicit root credentials. You can override this by setting environment variables:

```bash
export IGGY_ROOT_USERNAME=iggy
export IGGY_ROOT_PASSWORD=my-secret-password
```

Or use the `--with-default-root-credentials` flag for development (sets root credentials to `iggy`/`iggy`).

These variables initialize the root account only when it is first created. Supplied values are still validated on later starts, but they do not replace the stored credentials. Use the password-change API or `iggy user password` with the current password to change an existing account. The `--fresh` flag deletes all data; it is not needed for a password change.

The root user **cannot be deleted**, and its permissions are fixed: it always holds every permission.

### Personal Access Tokens (PAT)

PATs provide programmatic access with optional expiry. Each user can have up to `max_tokens_per_user` (default 100) stored tokens, including expired tokens until the cleaner removes them. Tokens are **hashed before storage** and can be revoked at any time.

```bash
# Create a PAT via CLI
IGGY_TOKEN=$(iggy --quiet -u iggy -p my-secret-password pat create my-token 7d)

# Use the PAT for authentication
iggy -t "$IGGY_TOKEN" stream list
```

An automatic cleaner removes expired tokens at a configurable interval.

Broker API commands require an authenticated session or bearer credential, except for ping and the login/refresh flows that establish or prove a credential. The Prometheus `/metrics` scrape requires a JWT or personal access token. HTTP CORS preflight responses and embedded `/ui` static assets are public; the UI's broker API calls still require authentication.

## Authorization

Iggy provides granular permissions at three levels:

1. **Global permissions** - apply to the whole server and all streams
2. **Stream permissions** - scoped to one stream and all its topics
3. **Topic permissions** - scoped to one topic within a stream

The root user has full access to everything and **bypasses all permission checks**. Other users can be assigned specific permissions using the CLI, HTTP API, SDK, or Web UI.

### Permission matrix

The global permissions and the operations they unlock:

| Operation | Required global permission |
|-----------|---------------------------|
| Get server stats | `read_servers` |
| List clients, get client | `read_servers` |
| Create, update, delete users; update permissions; change passwords | `manage_users` |
| Get user, list users | `read_users` |
| Create, update, delete streams | `manage_streams` |
| Get stream, list streams | `read_streams` |
| Create, update, delete topics | `manage_topics` |
| Get topic, list topics | `read_topics` |
| Consumer groups: create, delete, join, leave, get | `read_topics` |
| Poll messages | `poll_messages` |
| Send messages | `send_messages` |

Two kinds of implication apply on top of the table:

- **Supersets**: every `manage_*` permission includes its `read_*` counterpart. In addition `manage_streams` includes `manage_topics`, `read_streams` includes `read_topics`, and `read_topics` includes `poll_messages`. `manage_topics` also includes `send_messages`, so `manage_streams` permits sending too.
- **Self-service**: an authenticated user can always read their own account, change their own password, and manage their own personal access tokens, without any of the user permissions above.

### Scoped permissions

Instead of (or in addition to) global grants, permissions can be scoped per stream and per topic:

- **Stream scope**: `manage_stream`, `read_stream`, `manage_topics`, `read_topics`, `poll_messages`, `send_messages`, plus an optional per-topic map. `read_stream` includes `read_topics` and `poll_messages` within that stream.
- **Topic scope**: `manage_topic`, `read_topic`, `poll_messages`, `send_messages`. `read_topic` includes `poll_messages` on that topic.

### How permissions cascade

Permissions are checked from top to bottom: global, then stream, then topic. A permission granted at a higher level covers everything below it:

- Global `poll_messages` lets the user poll messages from **all** streams and topics.
- Stream-scoped `poll_messages` on stream 1 lets the user poll from **all topics** in stream 1, but not from other streams.
- Topic-scoped `poll_messages` on topic 5 of stream 1 lets the user poll **only** from that topic.

If a stream has no entry in the user's stream permissions, only global permissions apply to it. The same holds for topics within a stream.

For example, a user that may only consume from stream 42 needs no global permissions at all: grant stream-scoped `read_stream` and `poll_messages` on stream 42, and the user can read that stream, list its topics, and poll messages from any topic in it. The read grant also permits consumer-group operations in that stream. For polling without those read and group permissions, grant only `poll_messages`.

## Transport encryption (TLS)

Each transport configures TLS in its own section, and the `self_signed` semantics differ per transport:

| Transport | Section | Behavior |
|-----------|---------|----------|
| TCP | `[tcp.tls]` | `self_signed = true` generates an ephemeral certificate only while `cert_file` does not exist; an existing PEM pair is loaded instead. |
| WebSocket | `[websocket.tls]` | Same load-or-generate rule as TCP. |
| QUIC | `[quic.certificate]` | TLS is mandatory (part of the QUIC spec). `self_signed = true` **always** generates an ephemeral certificate and ignores `cert_file`/`key_file`, logging a warning if the files exist. |
| HTTP | `[http.tls]` | No `self_signed` option: HTTPS requires certificate and key files in `cert_file`/`key_file`. |

For production deployments, provide proper certificates via `cert_file` and `key_file`. Ephemeral certificates change on every start, so a trust configuration pinned to one generated certificate does not survive a restart. See [Networking](https://iggy.apache.org/docs/server/networking) for the surrounding transport configuration.

## Data encryption at rest

Iggy supports optional **AES-256-GCM** encryption for message payloads and user headers. They are encrypted before storage and decrypted for polling. Metadata journals, metadata snapshots, and structural record headers remain unencrypted. This option does not provide whole-directory encryption. The encryption key must decode from base64 to 32 bytes.

```toml
[encryption]
enabled = false
key = ""  # 32-byte base64-encoded key
```

## JWT (HTTP API)

The HTTP API uses JWT (JSON Web Tokens) for session management. Tokens are signed with HS256 by default and have configurable expiry, clock skew tolerance, and audience/issuer validation:

```toml
[http.jwt]
algorithm = "HS256"
issuer = "iggy.apache.org"
audience = "iggy.apache.org"
access_token_expiry = "1 h"
clock_skew = "5 s"
```

Further keys (`valid_issuers`, `valid_audiences`, `not_before`, `use_base64_secret`, the signing secrets) are covered in [Configuration](https://iggy.apache.org/docs/server/configuration).

**Signing secrets**: `encoding_secret` and `decoding_secret` default to empty. Without `cluster.auth`, the server generates a secure random secret on every start. That's a safe single-node default with two consequences: issued tokens **die on restart**, and in a cluster each node signs with its own key, so bearers are node-local and follower-to-primary request forwarding stays disabled. For clusters, configure an identical secret on every node (prefer the `IGGY_HTTP_JWT_ENCODING_SECRET`/`IGGY_HTTP_JWT_DECODING_SECRET` environment variables over on-disk config), or enable `cluster.auth` so the JWT key derives from the cluster's shared PSK.

**Refresh tokens**: `POST /users/refresh-token` re-issues an access token from a still-valid one presented in the request body, answering the same identity shape as login, so HTTP clients can extend a session without re-sending credentials.

**Federated issuers**: the opt-in `[[http.jwt.trusted_issuers]]` list accepts tokens minted by external identity providers for application-to-application flows. Each entry names an `issuer`, `audience`, and `jwks_url`. Signatures are verified against the issuer's JWKS (fetches are rate-limited), and every token from that issuer is remapped onto the configured non-root `user_id`. With no entries configured, the listener accepts only self-issued tokens. See [Configuration](https://iggy.apache.org/docs/server/configuration) for the key reference.

## Cluster security

In cluster mode, replica-to-replica traffic can require an authenticated handshake based on a pre-shared key (with a documented rolling rotation procedure) and can be wrapped in TLS 1.3. With `cluster.auth` enabled and no JWT secrets configured, the PSK also becomes the cluster-wide JWT key source. See [Clustering security](https://iggy.apache.org/docs/clustering/security).
