Skip to content

Authentication

Authentication is disabled by default. Enable it to secure your server and enforce per-app rate limits.

Enabling Authentication

Set require_websocket_auth to true and add authorized apps:

JSON
{
  "security": {
    "require_websocket_auth": true,
    "authorized_apps": [
      {
        "app_id": "my-game",
        "app_secret": "your-secret-here",
        "app_name": "My Game",
        "max_rooms": 100,
        "max_players_per_room": 16,
        "rate_limit_per_minute": 60
      }
    ]
  }
}

Important: Change the default app_secret before deploying to production. The example value CHANGE_ME_BEFORE_PRODUCTION in config.example.json is intentionally insecure.

Client Authentication

When auth is enabled, clients must send an Authenticate message immediately after connecting:

JavaScript
const ws = new WebSocket('ws://localhost:3536/v2/ws');

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'Authenticate',
    data: {
      app_id: 'my-game'
    }
  }));
};

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  if (message.type === 'Authenticated') {
    console.log('Authenticated successfully');
    // Now you can create/join rooms
  }

  if (message.type === 'Error' && message.data.error_code === 'AUTHENTICATION_REQUIRED') {
    console.error('Authentication failed');
  }
};

Per-App Settings

Each authorized app has its own limits:

JSON
{
  "app_id": "my-game",
  "app_secret": "secret-key",
  "app_name": "My Game",
  "max_rooms": 100,
  "max_players_per_room": 16,
  "rate_limit_per_minute": 60
}
  • app_id - Unique identifier for the app
  • app_secret - Secret key for authentication
  • app_name - Human-readable name (for logging/metrics)
  • max_rooms - Maximum concurrent rooms for this app
  • max_players_per_room - Max players per room for this app
  • rate_limit_per_minute - Max requests per minute per IP for this app

Auth Timeout

Clients must authenticate within the configured timeout:

JSON
{
  "websocket": {
    "auth_timeout_secs": 10
  }
}

If the client doesn't send Authenticate within this window, the connection is closed.

Metrics Authentication

Protect the /metrics endpoints:

JSON
{
  "security": {
    "require_metrics_auth": true
  }
}

When enabled, metrics endpoints require an Authorization header:

Bash
curl -H "Authorization: Bearer my-game:your-secret-here" \
  http://localhost:3536/metrics

Format: Bearer <app_id>:<app_secret>

Error Codes

Common auth-related errors:

  • AUTHENTICATION_REQUIRED - Authentication is required but not provided
  • INVALID_APP_ID - Invalid app ID
  • AUTHENTICATION_TIMEOUT - Client did not authenticate in time
  • MAX_ROOMS_PER_GAME_EXCEEDED - App has reached its max rooms limit

Example: Multiple Apps

JSON
{
  "security": {
    "require_websocket_auth": true,
    "authorized_apps": [
      {
        "app_id": "production-game",
        "app_secret": "prod-secret-key",
        "app_name": "Production Game",
        "max_rooms": 1000,
        "max_players_per_room": 16,
        "rate_limit_per_minute": 100
      },
      {
        "app_id": "dev-game",
        "app_secret": "dev-secret-key",
        "app_name": "Development Game",
        "max_rooms": 10,
        "max_players_per_room": 4,
        "rate_limit_per_minute": 20
      }
    ]
  }
}

Security Best Practices

  1. Never commit secrets - Use environment variables in production
  2. Generate strong secrets - Use a password generator or openssl rand -base64 32
  3. Rotate secrets regularly - Update secrets periodically
  4. Use HTTPS in production - Protect credentials in transit
  5. Monitor failed auth attempts - Watch for brute-force attacks

Environment Variables

Override app secrets via environment:

Bash
# Not recommended - shown for reference only
SIGNAL_FISH_SECURITY__AUTHORIZED_APPS='[{"app_id":"my-game","app_secret":"env-secret",...}]'

Better approaches for production secrets management:

Docker Secrets (Docker Swarm / Compose)

YAML
# docker-compose.yml
version: '3.8'
services:
  signal-fish:
    image: ghcr.io/ambiguous-interactive/signal-fish-server:latest
    secrets:

      - signal_fish_config

    entrypoint: sh -c "cp /run/secrets/signal_fish_config /app/config.json && /app/signal-fish-server"

secrets:
  signal_fish_config:
    file: ./config.secret.json

Kubernetes ConfigMap and Secrets

Bash
# Create secret from file
kubectl create secret generic signal-fish-config --from-file=config.json=./config.secret.json
YAML
# deployment.yaml
apiVersion: v1
kind: Deployment
metadata:
  name: signal-fish-server
spec:
  template:
    spec:
      containers:

      - name: signal-fish

        image: ghcr.io/ambiguous-interactive/signal-fish-server:latest
        volumeMounts:

        - name: config

          mountPath: /app/config.json
          subPath: config.json
          readOnly: true
      volumes:

      - name: config

        secret:
          secretName: signal-fish-config

Environment Variable Templating

Generate config.json at runtime from environment variables:

Bash
#!/bin/bash
# entrypoint.sh
cat > /app/config.json <<EOF
{
  "port": ${PORT:-3536},
  "security": {
    "require_websocket_auth": true,
    "authorized_apps": [
      {
        "app_id": "${APP_ID}",
        "app_secret": "${APP_SECRET}",
        "app_name": "${APP_NAME}",
        "max_rooms": ${MAX_ROOMS:-100},
        "max_players_per_room": ${MAX_PLAYERS:-16},
        "rate_limit_per_minute": ${RATE_LIMIT:-60}
      }
    ]
  }
}
EOF

exec /app/signal-fish-server

AWS Secrets Manager

Bash
# Fetch secret and write to config file
aws secretsmanager get-secret-value \
  --secret-id signal-fish-config \
  --query SecretString \
  --output text > /app/config.json

# Start server
/app/signal-fish-server

HashiCorp Vault

Bash
# Fetch secret from Vault
vault kv get -field=config secret/signal-fish > /app/config.json

# Start server
/app/signal-fish-server

Next Steps