# Authentication

Secure your Cntrl API with API keys and IP whitelisting.

## Security Warning

**Authentication alone may not make Cntrl safe for the public internet.** We strongly recommend against exposing port `9990` via router port forwarding. Use a VPN (Tailscale/WireGuard) for remote access.

By default, Cntrl's API is open to anyone on your network. If you are on a **shared network** (like a dorm or office) or using a VPN like Tailscale, you should enable authentication to secure endpoints.

## How it works

**Whitelisted IPs bypass the API key check entirely.** This means trusted devices (like a dashboard server) can use `EventSource` or WebSocket without custom headers, while external clients still need the API key.

## Quick Setup

Add this to your `config.json`:

```
{
  "auth": {
    "enabled": true,
    "api_key": "your-secret-key-here",
    "allowed_ips": ["192.168.1.100"],
    "blocked_ips": []
  }
}
```

The IP `192.168.1.100` (your dashboard server, for example) will bypass the API key check entirely.

## IP Blocking (Blacklist)

You can block specific IPs from accessing your API entirely. **Blocked IPs are checked first, even when authentication is disabled.**

```
{
  "auth": {
    "enabled": false,
    "blocked_ips": ["192.168.1.50", "10.0.0.0/8"]
  }
}
```

Blocked IPs Take Priority

If an IP appears in both `allowed_ips` and `blocked_ips`, it will be **blocked**. The blocklist is always checked first.

## Making Authenticated Requests

Include your API key in the `Authorization` header:

```
curl -H "Authorization: Bearer your-secret-key-here" \
     http://your-pc:9990/api/system
```

Public Endpoint

The `/api/status` endpoint is always public for health checks—no authentication required.

## IP Whitelisting

For additional security, you can restrict access to specific IP addresses:

```
{
  "auth": {
    "enabled": true,
    "api_key": "your-secret-key-here",
    "allowed_ips": ["192.168.1.100", "192.168.1.0/24"]
  }
}
```

### Supported Formats

| Format      | Example              | Description                |
|-------------|----------------------|----------------------------|
| Single IP  | `192.168.1.100`      | Exact IP match             |
| CIDR       | `192.168.1.0/24`     | Subnet range               |
| Wildcard   | `*`                  | Allow all IPs (only API key checked) |

## Common Configurations

### Tailscale Network

If both your PC and dashboard are on Tailscale:

```
{
  "auth": {
    "enabled": true,
    "api_key": "your-secret-key-here",
    "allowed_ips": ["100.64.0.0/10"]
  }
}
```

### Local Network Only

Restrict to your home network:

```
{
  "auth": {
    "enabled": true,
    "api_key": "your-secret-key-here",
    "allowed_ips": ["192.168.1.0/24"]
  }
}
```

### API Key Only (No IP Restriction)

Allow any IP with the correct key:

```
{
  "auth": {
    "enabled": true,
    "api_key": "your-secret-key-here",
    "allowed_ips": ["*"]
  }
}
```

## Dashboard Integration

### JavaScript / TypeScript

```
const API_KEY = "your-secret-key-here";
const API_URL = "http://your-pc:9990";

// Regular fetch
const response = await fetch(`${API_URL}/api/system`, {
  headers: {
    Authorization: `Bearer ${API_KEY}`,
  },
});

// SSE Stream
const eventSource = new EventSource(
  `${API_URL}/api/stream?fields=cpu,memory`,
  // Note: EventSource doesn't support custom headers natively
  // See workaround below
);
```

SSE and Authentication

The browser's `EventSource` API doesn't support custom headers. For authenticated SSE streams, you have two options:

1. **Use IP whitelisting** - Add your dashboard's IP to `allowed_ips` and skip the API key check for SSE
2. **Use a fetch-based SSE client** like [eventsource-parser](https://github.com/rexxars/eventsource-parser)

WebSocket Authentication

The WebSocket endpoint (`/api/ws`) follows the same authentication rules. Use IP whitelisting for browser WebSocket connections, or pass the API key via a query parameter if needed.

### Fetch-based SSE Example

```
async function streamWithAuth(url, apiKey, onMessage) {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });

const reader = response.body.getReader();
  const decoder = new TextDecoder();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;

const text = decoder.decode(value);
    const lines = text.split("\n");

for (const line of lines) {
      if (line.startsWith("data: ")) {
        onMessage(JSON.parse(line.slice(6)));
      }
    }
  }
}

// Usage
streamWithAuth(
  "http://your-pc:9990/api/stream?fields=cpu,memory",
  "your-secret-key-here",
  (data) => console.log(data),
);
```

## Error Responses

| Status                   | Meaning                                                |
|--------------------------|--------------------------------------------------------|
| `401 Unauthorized`       | Missing or invalid API key                             |
| `403 Forbidden`          | IP is blocked, or IP not in whitelist (when enabled)  |

No Error Body

For security reasons, `401` and `403` responses do not include a JSON body with details.

## Generating a Secure Key

Use a random string generator to create a strong API key:

```
# Linux/macOS
openssl rand -base64 32

# PowerShell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
```

### Key Length

A 32-character random string is more than enough for a home network. For production, consider using a UUID or a 64-character hex string.
