{"slug":"serverless-authentication","title":"Serverless Container Authentication","description":"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...","section":"Features","url":"https://docs.danubedata.ro/serverless-authentication","markdown_url":"https://docs.danubedata.ro/serverless-authentication.md","breadcrumbs":[{"title":"Features","slug":null},{"title":"Rapids","slug":"serverless-overview"},{"title":"Authentication","slug":"serverless-authentication"}],"headings":[{"level":1,"title":"Serverless Container Authentication","id":"serverless-container-authentication"},{"level":2,"title":"Authentication approaches","id":"authentication-approaches"},{"level":3,"title":"No authentication (public endpoints)","id":"no-authentication-public-endpoints"},{"level":3,"title":"Bearer token authentication","id":"bearer-token-authentication"},{"level":3,"title":"API key via query parameter or custom header","id":"api-key-via-query-parameter-or-custom-header"},{"level":3,"title":"JWT (JSON Web Tokens)","id":"jwt-json-web-tokens"},{"level":3,"title":"OAuth 2.0 / OpenID Connect","id":"oauth-20-openid-connect"},{"level":2,"title":"Environment variables for secrets","id":"environment-variables-for-secrets"},{"level":2,"title":"Protecting specific routes","id":"protecting-specific-routes"},{"level":2,"title":"IP-based restrictions","id":"ip-based-restrictions"},{"level":2,"title":"Best practices","id":"best-practices"},{"level":2,"title":"Next steps","id":"next-steps"}],"format":"markdown","word_count":423,"content":"# Serverless Container Authentication\n\nDanubeData 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.\n\nFor 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.\n\n## Authentication approaches\n\n### No authentication (public endpoints)\n\nIf 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.\n\n```bash\ncurl https://my-api-acme.danubedata.run/health\n```\n\n### Bearer token authentication\n\nThe most common approach for APIs. Your application validates a token sent in the `Authorization` header.\n\n**How it works:**\n\n1. Generate an API token (a random string) and store it as an environment variable in your container configuration.\n2. In your application, check incoming requests for the `Authorization: Bearer <token>` header.\n3. Reject requests with a missing or invalid token.\n\n**Setting the token:**\n\n1. Navigate to your container's **Edit** page\n2. Under **Environment Variables**, add a variable:\n   - **Key:** `API_TOKEN` (or any name you prefer)\n   - **Value:** A strong, random string (e.g., generate one with `openssl rand -hex 32`)\n3. Save and redeploy\n\n**Example: Node.js (Express)**\n\n```javascript\nconst API_TOKEN = process.env.API_TOKEN;\n\napp.use((req, res, next) => {\n    const authHeader = req.headers.authorization;\n\n    if (!authHeader || !authHeader.startsWith('Bearer ')) {\n        return res.status(401).json({ error: 'Missing authorization header' });\n    }\n\n    const token = authHeader.split(' ')[1];\n    if (token !== API_TOKEN) {\n        return res.status(401).json({ error: 'Invalid token' });\n    }\n\n    next();\n});\n```\n\n**Example: Python (Flask)**\n\n```python\nimport os\nfrom functools import wraps\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nAPI_TOKEN = os.environ.get('API_TOKEN')\n\ndef require_auth(f):\n    @wraps(f)\n    def decorated(*args, **kwargs):\n        auth_header = request.headers.get('Authorization', '')\n        if not auth_header.startswith('Bearer '):\n            return jsonify({'error': 'Missing authorization header'}), 401\n        token = auth_header.split(' ', 1)[1]\n        if token != API_TOKEN:\n            return jsonify({'error': 'Invalid token'}), 401\n        return f(*args, **kwargs)\n    return decorated\n\n@app.route('/api/data')\n@require_auth\ndef get_data():\n    return jsonify({'message': 'Authenticated'})\n```\n\n**Example: Go**\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"os\"\n    \"strings\"\n)\n\nvar apiToken = os.Getenv(\"API_TOKEN\")\n\nfunc authMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        auth := r.Header.Get(\"Authorization\")\n        if !strings.HasPrefix(auth, \"Bearer \") {\n            http.Error(w, `{\"error\":\"Missing authorization header\"}`, http.StatusUnauthorized)\n            return\n        }\n        token := strings.TrimPrefix(auth, \"Bearer \")\n        if token != apiToken {\n            http.Error(w, `{\"error\":\"Invalid token\"}`, http.StatusUnauthorized)\n            return\n        }\n        next.ServeHTTP(w, r)\n    })\n}\n```\n\n**Example: PHP**\n\n```php\n<?php\n$apiToken = getenv('API_TOKEN');\n\n$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';\n\nif (!str_starts_with($authHeader, 'Bearer ')) {\n    http_response_code(401);\n    echo json_encode(['error' => 'Missing authorization header']);\n    exit;\n}\n\n$token = substr($authHeader, 7);\nif ($token !== $apiToken) {\n    http_response_code(401);\n    echo json_encode(['error' => 'Invalid token']);\n    exit;\n}\n```\n\n**Calling an authenticated endpoint:**\n\n```bash\ncurl https://my-api-acme.danubedata.run/api/data \\\n  -H \"Authorization: Bearer your-secret-token-here\"\n```\n\n### API key via query parameter or custom header\n\nFor simpler integrations (e.g., webhooks from third-party services), you can accept an API key as a query parameter or custom header.\n\n```bash\n# Via query parameter\ncurl \"https://my-api-acme.danubedata.run/webhook?api_key=your-key\"\n\n# Via custom header\ncurl https://my-api-acme.danubedata.run/webhook \\\n  -H \"X-API-Key: your-key\"\n```\n\nStore the expected key as an environment variable and validate it in your application.\n\n### JWT (JSON Web Tokens)\n\nFor applications with user accounts, use JWT tokens for stateless authentication. Your container validates the token signature using a shared secret or public key.\n\n1. Store the JWT secret as an environment variable (`JWT_SECRET`)\n2. Issue tokens from your authentication service\n3. Validate tokens in your container on each request\n\nLibraries like `jsonwebtoken` (Node.js), `PyJWT` (Python), and `firebase/php-jwt` (PHP) handle JWT validation.\n\n### OAuth 2.0 / OpenID Connect\n\nFor 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.\n\n## Environment variables for secrets\n\nStore all authentication secrets as environment variables — never hardcode them in your application code or container image.\n\n**Adding environment variables:**\n\n1. Navigate to the container **Edit** page\n2. Scroll to **Environment Variables**\n3. Add your key-value pairs:\n   - `API_TOKEN` — Your authentication token\n   - `JWT_SECRET` — Your JWT signing secret\n   - `OAUTH_CLIENT_SECRET` — Your OAuth client secret\n4. Save and redeploy\n\nEnvironment variables are encrypted at rest and injected into your container at startup. They are never exposed in logs or the container image.\n\n## Protecting specific routes\n\nYou don't have to protect every endpoint. A common pattern is to leave health check and public routes open while protecting API routes:\n\n```javascript\n// Public routes — no auth required\napp.get('/health', (req, res) => res.json({ status: 'ok' }));\napp.get('/', (req, res) => res.json({ name: 'My API', version: '1.0' }));\n\n// Protected routes — require Bearer token\napp.use('/api', authMiddleware);\napp.get('/api/data', (req, res) => { /* ... */ });\napp.post('/api/actions', (req, res) => { /* ... */ });\n```\n\n## IP-based restrictions\n\nYour 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:\n\n```javascript\nconst ALLOWED_IPS = process.env.ALLOWED_IPS?.split(',') || [];\n\n// Internal hops appended by our platform. Their number varies (a custom domain\n// adds one more than the *.danubedata.run URL), so discard by range, not position.\nconst INTERNAL = /^(10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|::1|fe80:|f[cd])/i;\n\nfunction clientIp(req) {\n    const chain = (req.headers['x-forwarded-for'] || '').split(',').map((s) => s.trim());\n    for (let i = chain.length - 1; i >= 0; i--) {\n        if (chain[i] && !INTERNAL.test(chain[i])) return chain[i];\n    }\n    return null;\n}\n\nfunction ipWhitelist(req, res, next) {\n    const ip = clientIp(req);\n    if (ALLOWED_IPS.length > 0 && (!ip || !ALLOWED_IPS.includes(ip))) {\n        return res.status(403).json({ error: 'Forbidden' });\n    }\n    next();\n}\n```\n\nSet `ALLOWED_IPS` as a comma-separated environment variable (e.g., `203.0.113.10,198.51.100.20`).\n\nThis 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.\n\n> **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).\n\n## Best practices\n\n- **Use environment variables** for all secrets. Never hardcode tokens in source code.\n- **Generate strong tokens** with at least 32 bytes of randomness (`openssl rand -hex 32`).\n- **Rotate tokens periodically.** Update the environment variable and redeploy.\n- **Return `401 Unauthorized`** for missing or invalid credentials, not `403 Forbidden`.\n- **Use HTTPS only.** All DanubeData container URLs use TLS by default — never disable it.\n- **Keep health checks public.** Monitoring and load balancers need unauthenticated access to `/health` or similar endpoints.\n- **Log authentication failures** to detect abuse, but never log the token values themselves.\n\n## Next steps\n\n- [Invocation guide](https://docs.danubedata.ro/serverless-invoking) — Code examples for calling your container\n- [Custom domains](https://docs.danubedata.ro/serverless-domains) — Add your own domain with automatic TLS\n\n---\n\n**Questions?** Contact support at support@danubedata.ro\n","prev":{"title":"Invoking Containers","slug":"serverless-invoking","url":"https://docs.danubedata.ro/serverless-invoking","markdown_url":"https://docs.danubedata.ro/serverless-invoking.md","json_url":"https://docs.danubedata.ro/serverless-invoking.json"},"next":{"title":"Custom Domains","slug":"serverless-domains","url":"https://docs.danubedata.ro/serverless-domains","markdown_url":"https://docs.danubedata.ro/serverless-domains.md","json_url":"https://docs.danubedata.ro/serverless-domains.json"},"index_url":"https://docs.danubedata.ro/index.json"}