Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ Always run `make ready` before committing changes.
"github.com/puzpuzpuz/xsync/v4" // Concurrent maps/counters
"github.com/tidwall/gjson" // Fast JSON parsing
"github.com/jellydator/ttlcache" // Time-to-live cache
"github.com/rs/cors" // CORS middleware for browser clients
"golang.org/x/sync" // errgroup
"golang.org/x/time" // rate limiting
```
Expand Down
24 changes: 24 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ server:
write_timeout: 0s # for LLMs streaming, leave this as 0s
shutdown_timeout: 10s # Graceful shutdown timeout, increase this for heavy sites that have long-running requests or lots of endpoints (30s is a good)
request_logging: true # logs Http Requests, may be useful for debugging, noise for other use cases

# CORS (Cross-Origin Resource Sharing) for browser-based clients (OpenWebUI, dashboards).
# Off by default. Non-browser clients (curl, SDKs, coding agents) ignore CORS entirely,
# so enabling this has no effect on them. When a request carries no Origin header it is
# passed through untouched.
#
# Caveat: allow_credentials: true is incompatible with allowed_origins: ["*"] (the CORS
# spec forbids it and browsers reject the response). List explicit origins when using
# credentials. exposed_headers left empty auto-exposes the full X-Olla-* response header
# set, so browser clients can read routing/model metadata.
cors:
enabled: false # opt-in: only enable when browser clients need access
allowed_origins:
- "*"
allowed_methods:
- "GET"
- "POST"
- "OPTIONS"
allowed_headers:
- "*"
exposed_headers: [] # empty: auto-exposes the X-Olla-* headers
allow_credentials: false # set true only with explicit allowed_origins (not "*")
max_age: 300 # preflight cache in seconds (5 minutes)

request_limits:
max_body_size: 52428800 # 50MB
max_header_size: 524288 # 512KB
Expand Down
8 changes: 4 additions & 4 deletions docs/content/api-reference/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ For streaming endpoints (chat completions, text generation), responses use:

## CORS Support

CORS headers are included for browser-based clients:
CORS is **disabled by default**. Most clients (CLI tools, SDKs, coding agents, and server-side apps such as OpenWebUI's backend) send no `Origin` header, so CORS does not apply to them.

- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Methods: GET, POST, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type, Authorization`
Enable it only when a browser connects directly to Olla, for example a custom web dashboard or a UI configured for browser-direct connections. Once enabled, Olla answers preflight requests automatically and, by default, exposes the full `X-Olla-*` response header set so browser JavaScript can read routing and model metadata.

The allowed origins, methods, headers, exposed headers, credentials, and preflight cache are all configurable. See [Security Best Practices - CORS](../configuration/practices/security.md#cors) and the [Configuration Reference](../configuration/reference.md#cors) for details.
16 changes: 16 additions & 0 deletions docs/content/configuration/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ server:

See [Rate Limiting Reference](reference.md#rate-limiting) for complete details.

### CORS

CORS is disabled by default. Enable it only when browser clients (such as OpenWebUI or a custom dashboard) connect directly to Olla. Non-browser clients are unaffected.

```yaml
server:
cors:
enabled: true
allowed_origins:
- "https://my-dashboard.example.com"
allow_credentials: true
max_age: 600
```
Comment on lines +150 to +158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use indented code block style to satisfy markdownlint (MD046).

This fenced YAML block breaks the configured code-block-style rule and may keep docs lint noisy or failing. Convert this block to indented style to match the repository convention.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 150-150: Code block style
Expected: indented; Actual: fenced

(MD046, code-block-style)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/configuration/overview.md` around lines 150 - 158, Replace the
fenced YAML code block with an indented code block to satisfy MD046: remove the
triple-backtick fence and indent each line of the YAML snippet (starting with
"server:" and including nested "cors:", "enabled:", "allowed_origins:", etc.) by
at least four spaces so the YAML remains formatted as an indented code block
within docs/content/configuration/overview.md.

Source: Linters/SAST tools


See [Security Best Practices](practices/security.md#cors) for the full configuration reference and credential/wildcard caveat.

Endpoints also support per-endpoint outbound authentication (`auth:`) and custom headers (`headers:`). See [Endpoint Authentication](endpoint-auth.md) for configuration details.

## Proxy Configuration
Expand Down
84 changes: 84 additions & 0 deletions docs/content/configuration/practices/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,89 @@ Any header named in an endpoint's `auth.header` field or in the `headers:` map i
!!! note "Custom header names"
If you configure a non-standard credential header (e.g. `auth.header: X-My-Token`), Olla strips `X-My-Token` from responses as well. No additional configuration is needed.

## CORS {#cors}

CORS is **disabled by default**. It is only relevant when a browser client (OpenWebUI, a custom dashboard, or a web app) connects directly to Olla. CLI tools, SDKs, and coding agents send no `Origin` header and pass through Olla completely untouched regardless of this setting.

### When to enable

Enable CORS when:

- You are running a browser-based UI (e.g. OpenWebUI) that talks directly to Olla rather than through a reverse proxy that handles CORS itself.
- You have a custom JavaScript dashboard consuming Olla's API.

Do **not** enable CORS when all clients are server-side or CLI-based -- it adds no value and broadens the attack surface.

### Permissive configuration (development)

Suitable for local development where any origin should be allowed:

```yaml
server:
cors:
enabled: true
allowed_origins:
- "*"
allowed_methods:
- "GET"
- "POST"
- "OPTIONS"
allowed_headers:
- "*"
max_age: 300
```

### Locked-down configuration (production)

Restrict to known origins and expose only the headers your UI needs to read:

```yaml
server:
cors:
enabled: true
allowed_origins:
- "https://my-dashboard.example.com"
allowed_methods:
- "GET"
- "POST"
- "OPTIONS"
allowed_headers:
- "Authorization"
- "Content-Type"
allow_credentials: true
max_age: 600
```

!!! warning "Credentials + wildcard origin"
Setting `allow_credentials: true` alongside `allowed_origins: ["*"]` is forbidden by the CORS specification. Olla rejects this combination at startup with a fatal error. Always list explicit origins when enabling credentials.

### Exposed headers

When `exposed_headers` is left empty (the default), Olla automatically exposes the full `X-Olla-*` response header set to browser clients:

- `X-Olla-Endpoint`, `X-Olla-Model`, `X-Olla-Backend-Type`, `X-Olla-Request-ID`, `X-Olla-Response-Time`
- Routing headers: `X-Olla-Routing-Strategy`, `X-Olla-Routing-Decision`, `X-Olla-Routing-Reason`
- Sticky session headers: `X-Olla-Sticky-Session`, `X-Olla-Sticky-Key-Source`, `X-Olla-Session-ID`

This means browser JavaScript can read routing and model metadata without any additional configuration. Override by listing specific headers in `exposed_headers` if you want to restrict what the browser can access.

### Environment variable overrides

| Variable | Type | Example |
|----------|------|---------|
| `OLLA_SERVER_CORS_ENABLED` | bool | `true` |
| `OLLA_SERVER_CORS_ALLOWED_ORIGINS` | comma-separated | `https://app.example.com,https://admin.example.com` |
| `OLLA_SERVER_CORS_ALLOWED_METHODS` | comma-separated | `GET,POST,OPTIONS` |
| `OLLA_SERVER_CORS_ALLOWED_HEADERS` | comma-separated | `Authorization,Content-Type` |
| `OLLA_SERVER_CORS_EXPOSED_HEADERS` | comma-separated | `X-Olla-Model,X-Olla-Endpoint` |
| `OLLA_SERVER_CORS_ALLOW_CREDENTIALS` | bool | `true` |
| `OLLA_SERVER_CORS_MAX_AGE` | int (seconds) | `600` |

!!! note "No spaces in comma-separated values"
Env var lists use commas with no surrounding spaces: `https://a.com,https://b.com` not `https://a.com, https://b.com`.

See [Configuration Reference](../reference.md#cors) for the full field reference.

## Secrets Resolution

Credential values in `auth:` and `headers:` blocks support two forms:
Expand Down Expand Up @@ -454,6 +537,7 @@ Production deployment checklist:
- [ ] Implement log rotation
- [ ] Set up alerting
- [ ] Document security procedures
- [ ] Enable CORS only if browser clients connect directly; use explicit origins in production

## Incident Response

Expand Down
29 changes: 29 additions & 0 deletions docs/content/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ server:
- "172.16.0.0/12"
```

### CORS {#cors}

Cross-Origin Resource Sharing settings. Only relevant when browser clients (OpenWebUI, custom dashboards) connect directly to Olla. Disabled by default; non-browser clients (curl, SDKs, coding agents) are unaffected regardless of this setting.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `cors.enabled` | bool | `false` | Enable CORS middleware |
| `cors.allowed_origins` | []string | `["*"]` | Permitted origins. Must be explicit URLs when `allow_credentials` is `true` |
| `cors.allowed_methods` | []string | `["GET","POST","OPTIONS"]` | Permitted HTTP methods |
| `cors.allowed_headers` | []string | `["*"]` | Permitted request headers |
| `cors.exposed_headers` | []string | `[]` | Response headers exposed to browser JS. Empty = auto-expose full `X-Olla-*` set |
| `cors.allow_credentials` | bool | `false` | Send `Access-Control-Allow-Credentials: true` |
| `cors.max_age` | int | `300` | Preflight cache duration in seconds |

!!! warning "Credentials + wildcard origin"
Setting `allow_credentials: true` with `allowed_origins: ["*"]` is forbidden by the CORS spec. Olla rejects this combination at startup with a fatal error. List explicit origins when credentials are required.

Example:

```yaml
server:
cors:
enabled: true
allowed_origins:
- "http://localhost:3000"
allow_credentials: true
max_age: 600
```

## Proxy Configuration

Proxy engine and request handling settings.
Expand Down
10 changes: 10 additions & 0 deletions docs/content/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ OLLA_LOG_LEVEL=debug

However, some settings like `proxy.profile` must be set in the YAML configuration file.

### Do I need to enable CORS?

Only if a browser connects **directly** to Olla, such as a custom web dashboard or a UI configured for browser-direct connections. CORS is disabled by default.

You do **not** need CORS for CLI tools, SDKs, coding agents, or server-side apps. This includes the standard OpenWebUI setup, where OpenWebUI's own backend calls Olla server-to-server (no browser `Origin` is involved). If Olla sits behind a reverse proxy (nginx, Traefik), handle CORS there instead.

When you do enable it, list explicit origins rather than `*` if you also set `allow_credentials: true` (the combination is forbidden by the CORS spec and Olla rejects it at startup). See [CORS configuration](configuration/practices/security.md#cors).

## Troubleshooting

### Streaming responses arrive all at once
Expand Down Expand Up @@ -323,6 +331,8 @@ Olla adds several headers to responses:

If missing, check you're using the `/olla/` prefix in your requests.

If a **browser** client cannot read these headers (server-side clients are unaffected), the browser is hiding them, not Olla. Cross-origin JavaScript can only read response headers that are explicitly exposed. Enable CORS and leave `exposed_headers` empty to auto-expose the full `X-Olla-*` set. See [CORS configuration](configuration/practices/security.md#cors).

### Connection refused errors

Common causes:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Olla works alongside API gateways like [LiteLLM](https://github.com/BerriAI/lite
- **Intelligent Load Balancing**: Priority-based, round-robin, and least-connections strategies
- **Health Monitoring**: Circuit breakers and automatic failover
- **High Performance**: Connection pooling, object pooling, and lock-free statistics
- **Security**: Built-in rate limiting and request validation
- **Security**: Built-in rate limiting, request validation, and optional CORS for browser clients
- **Observability**: Comprehensive metrics and request tracing
- **API Translation**: [Anthropic Messages API](concepts/api-translation.md) support for Claude-compatible clients

Expand Down
3 changes: 3 additions & 0 deletions docs/content/integrations/frontend/openwebui.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export OLLAMA_BASE_URL="http://localhost:40114/olla/ollama"

You can find an example integration of OpenWebUI with Olla and Ollama instances in <code>examples/ollama-openwebui</code> - see [latest in Github](https://github.com/thushan/olla/tree/main/examples/ollama-openwebui).

!!! note "CORS is not required for this setup"
OpenWebUI's backend connects to Olla server-to-server (the `OLLAMA_BASE_URL` above is read by the OpenWebUI server, not the browser), so no browser `Origin` reaches Olla and CORS does not apply. You only need to enable [Olla's CORS support](../../configuration/practices/security.md#cors) if a browser connects **directly** to Olla, such as a UI configured for browser-direct connections.

## Overview

<table>
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/mattn/go-isatty v0.0.22
github.com/pterm/pterm v0.12.83
github.com/puzpuzpuz/xsync/v4 v4.5.0
github.com/rs/cors v1.11.1
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.19.0
golang.org/x/sync v0.19.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeI
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
50 changes: 50 additions & 0 deletions internal/app/middleware/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package middleware

import (
"github.com/rs/cors"
"github.com/thushan/olla/internal/config"
"github.com/thushan/olla/internal/core/constants"
)

// DefaultCORSExposedHeaders is the set of X-Olla-* response headers exposed to
// browser clients when the caller has not configured explicit ExposedHeaders.
//
// By default browsers block non-simple response headers from cross-origin reads.
// Olla's routing, model, and sticky-session metadata all live in X-Olla-* headers,
// so browser clients (OpenWebUI, custom dashboards) cannot inspect them without
// this exposure list. We expose the full set rather than a subset to avoid
// surprising omissions when new headers are added to the proxy output.
var DefaultCORSExposedHeaders = []string{
constants.HeaderXOllaRequestID,
constants.HeaderXOllaEndpoint,
constants.HeaderXOllaBackendType,
constants.HeaderXOllaModel,
constants.HeaderXOllaResponseTime,
constants.HeaderXOllaRoutingStrategy,
constants.HeaderXOllaRoutingDecision,
constants.HeaderXOllaRoutingReason,
constants.HeaderXOllaMode,
constants.HeaderXOllaStickySession,
constants.HeaderXOllaStickyKeySource,
constants.HeaderXOllaSessionID,
}

// NewCORS builds an rs/cors handler from Olla's CORS config. It is only constructed
// when CORS is enabled (the caller gates on cfg.Enabled). When ExposedHeaders is empty
// we expose the full X-Olla-* response header set so browser clients can read Olla's
// routing/model metadata, which they otherwise cannot access cross-origin.
func NewCORS(cfg config.CorsConfig) *cors.Cors {
exposed := cfg.ExposedHeaders
if len(exposed) == 0 {
exposed = DefaultCORSExposedHeaders
}

return cors.New(cors.Options{
AllowedOrigins: cfg.AllowedOrigins,
AllowedMethods: cfg.AllowedMethods,
AllowedHeaders: cfg.AllowedHeaders,
ExposedHeaders: exposed,
AllowCredentials: cfg.AllowCredentials,
MaxAge: cfg.MaxAge,
})
}
Loading
Loading