# Serverless Container Authentication

DanubeData serverless containers are publicly accessible by default — any HTTP client can reach your container's URL. If your container serves a public API or website, no additional authentication is needed.

For private endpoints, you are responsible for implementing authentication inside your application. DanubeData passes all request headers through to your container, so you can use any standard authentication mechanism.

## Authentication approaches

### No authentication (public endpoints)

If your container is a public API, webhook receiver, or website, you don't need to configure any authentication. All requests are forwarded directly to your container.

```bash
curl https://my-api-acme.danubedata.run/health
```

### Bearer token authentication

The most common approach for APIs. Your application validates a token sent in the `Authorization` header.

**How it works:**

1. Generate an API token (a random string) and store it as an environment variable in your container configuration.
2. In your application, check incoming requests for the `Authorization: Bearer <token>` header.
3. Reject requests with a missing or invalid token.

**Setting the token:**

1. Navigate to your container's **Edit** page
2. Under **Environment Variables**, add a variable:
   - **Key:** `API_TOKEN` (or any name you prefer)
   - **Value:** A strong, random string (e.g., generate one with `openssl rand -hex 32`)
3. Save and redeploy

**Example: Node.js (Express)**

```javascript
const API_TOKEN = process.env.API_TOKEN;

app.use((req, res, next) => {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Missing authorization header' });
    }

    const token = authHeader.split(' ')[1];
    if (token !== API_TOKEN) {
        return res.status(401).json({ error: 'Invalid token' });
    }

    next();
});
```

**Example: Python (Flask)**

```python
import os
from functools import wraps
from flask import Flask, request, jsonify

app = Flask(__name__)
API_TOKEN = os.environ.get('API_TOKEN')

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Missing authorization header'}), 401
        token = auth_header.split(' ', 1)[1]
        if token != API_TOKEN:
            return jsonify({'error': 'Invalid token'}), 401
        return f(*args, **kwargs)
    return decorated

@app.route('/api/data')
@require_auth
def get_data():
    return jsonify({'message': 'Authenticated'})
```

**Example: Go**

```go
package main

import (
    "net/http"
    "os"
    "strings"
)

var apiToken = os.Getenv("API_TOKEN")

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if !strings.HasPrefix(auth, "Bearer ") {
            http.Error(w, `{"error":"Missing authorization header"}`, http.StatusUnauthorized)
            return
        }
        token := strings.TrimPrefix(auth, "Bearer ")
        if token != apiToken {
            http.Error(w, `{"error":"Invalid token"}`, http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}
```

**Example: PHP**

```php
<?php
$apiToken = getenv('API_TOKEN');

$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

if (!str_starts_with($authHeader, 'Bearer ')) {
    http_response_code(401);
    echo json_encode(['error' => 'Missing authorization header']);
    exit;
}

$token = substr($authHeader, 7);
if ($token !== $apiToken) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid token']);
    exit;
}
```

**Calling an authenticated endpoint:**

```bash
curl https://my-api-acme.danubedata.run/api/data \
  -H "Authorization: Bearer your-secret-token-here"
```

### API key via query parameter or custom header

For simpler integrations (e.g., webhooks from third-party services), you can accept an API key as a query parameter or custom header.

```bash
# Via query parameter
curl "https://my-api-acme.danubedata.run/webhook?api_key=your-key"

# Via custom header
curl https://my-api-acme.danubedata.run/webhook \
  -H "X-API-Key: your-key"
```

Store the expected key as an environment variable and validate it in your application.

### JWT (JSON Web Tokens)

For applications with user accounts, use JWT tokens for stateless authentication. Your container validates the token signature using a shared secret or public key.

1. Store the JWT secret as an environment variable (`JWT_SECRET`)
2. Issue tokens from your authentication service
3. Validate tokens in your container on each request

Libraries like `jsonwebtoken` (Node.js), `PyJWT` (Python), and `firebase/php-jwt` (PHP) handle JWT validation.

### OAuth 2.0 / OpenID Connect

For applications that integrate with identity providers (Google, GitHub, Auth0, etc.), implement the OAuth 2.0 flow in your container. Store the client ID and client secret as environment variables.

## Environment variables for secrets

Store all authentication secrets as environment variables — never hardcode them in your application code or container image.

**Adding environment variables:**

1. Navigate to the container **Edit** page
2. Scroll to **Environment Variables**
3. Add your key-value pairs:
   - `API_TOKEN` — Your authentication token
   - `JWT_SECRET` — Your JWT signing secret
   - `OAUTH_CLIENT_SECRET` — Your OAuth client secret
4. Save and redeploy

Environment variables are encrypted at rest and injected into your container at startup. They are never exposed in logs or the container image.

## Protecting specific routes

You don't have to protect every endpoint. A common pattern is to leave health check and public routes open while protecting API routes:

```javascript
// Public routes — no auth required
app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.get('/', (req, res) => res.json({ name: 'My API', version: '1.0' }));

// Protected routes — require Bearer token
app.use('/api', authMiddleware);
app.get('/api/data', (req, res) => { /* ... */ });
app.post('/api/actions', (req, res) => { /* ... */ });
```

## IP-based restrictions

Your container receives the caller's address in the `X-Forwarded-For` header. Resolve it by walking the chain from the right and taking the first public address — never the leftmost entry, which is whatever the caller sent:

```javascript
const ALLOWED_IPS = process.env.ALLOWED_IPS?.split(',') || [];

// Internal hops appended by our platform. Their number varies (a custom domain
// adds one more than the *.danubedata.run URL), so discard by range, not position.
const INTERNAL = /^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|::1|fe80:|f[cd])/i;

function clientIp(req) {
    const chain = (req.headers['x-forwarded-for'] || '').split(',').map((s) => s.trim());
    for (let i = chain.length - 1; i >= 0; i--) {
        if (chain[i] && !INTERNAL.test(chain[i])) return chain[i];
    }
    return null;
}

function ipWhitelist(req, res, next) {
    const ip = clientIp(req);
    if (ALLOWED_IPS.length > 0 && (!ip || !ALLOWED_IPS.includes(ip))) {
        return res.status(403).json({ error: 'Forbidden' });
    }
    next();
}
```

Set `ALLOWED_IPS` as a comma-separated environment variable (e.g., `203.0.113.10,198.51.100.20`).

This is safe against forgery: a caller who sends `X-Forwarded-For: 203.0.113.10` to impersonate an allowed address produces `203.0.113.10, <their real IP>, …`, and scanning from the right returns their real address, not the forged one.

> **Warning:** Do not build an allowlist from `x-forwarded-for.split(',')[0]`, from `X-Real-IP`, or from `X-Envoy-External-Address`. The first two are attacker-controlled — `X-Forwarded-For` is appended to rather than replaced, and `X-Real-IP` is forwarded exactly as sent — so a caller can bypass the check by supplying an allowed address. `X-Envoy-External-Address` is not forgeable, but it resolves to an *internal* address when the container is reached through a custom domain, so an allowlist built on it will reject everyone. See [the client IP guide](https://docs.danubedata.ro/serverless-invoking#getting-the-real-client-ip).

## Best practices

- **Use environment variables** for all secrets. Never hardcode tokens in source code.
- **Generate strong tokens** with at least 32 bytes of randomness (`openssl rand -hex 32`).
- **Rotate tokens periodically.** Update the environment variable and redeploy.
- **Return `401 Unauthorized`** for missing or invalid credentials, not `403 Forbidden`.
- **Use HTTPS only.** All DanubeData container URLs use TLS by default — never disable it.
- **Keep health checks public.** Monitoring and load balancers need unauthenticated access to `/health` or similar endpoints.
- **Log authentication failures** to detect abuse, but never log the token values themselves.

## Next steps

- [Invocation guide](https://docs.danubedata.ro/serverless-invoking) — Code examples for calling your container
- [Custom domains](https://docs.danubedata.ro/serverless-domains) — Add your own domain with automatic TLS

---

**Questions?** Contact support at support@danubedata.ro
