StellarStack
Architecture

Authentication & Authorization

Multi-layered authentication system in StellarStack

Authentication & Authorization

StellarStack uses a multi-layered authentication system to secure all components.

Authentication Layers

  1. User Authentication - Better Auth (users accessing the platform)
  2. Daemon Authentication - mTLS + API tokens (nodes connecting to control plane)
  3. Console Authentication - Signed JWTs (users accessing server consoles)

User Authentication (Better Auth)

Configuration

// apps/api/src/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "./db";

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  session: {
    strategy: "jwt", // or "database" for more control
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // Update session every 24 hours
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
    discord: {
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    },
  },
});

Session Flow

  1. User signs in via Better Auth
  2. Better Auth creates session (stored in Redis/DB)
  3. Session token returned as HTTP-only cookie
  4. All API requests include cookie automatically
  5. Hono middleware validates session on each request

API Middleware

// apps/api/src/middleware/auth.ts
import { auth } from "../auth";

export const authMiddleware = async (c, next) => {
  const session = await auth.api.getSession({
    headers: c.req.raw.headers,
  });

  if (!session) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  c.set("user", session.user);
  c.set("session", session.session);

  await next();
};

Role-Based Access Control (RBAC)

Role Hierarchy

Super Admin

    ├── Admin
    │     │
    │     ├── Moderator
    │     │
    │     └── Support

    └── User (default)

          └── Subuser (per-server)

Permission Matrix

export const PERMISSIONS = {
  // Server permissions
  "server.view": "View server details",
  "server.console": "Access server console",
  "server.console.send": "Send console commands",
  "server.files.read": "Read server files",
  "server.files.write": "Write server files",
  "server.files.delete": "Delete server files",
  "server.start": "Start server",
  "server.stop": "Stop server",
  "server.restart": "Restart server",
  "server.kill": "Force kill server",
  "server.delete": "Delete server",
  "server.settings": "Modify server settings",
  "server.subusers": "Manage subusers",
  "server.schedules": "Manage schedules",
  "server.backups": "Manage backups",
  "server.databases": "Manage databases",

  // Node permissions (admin only)
  "node.view": "View node details",
  "node.create": "Create nodes",
  "node.delete": "Delete nodes",
  "node.settings": "Modify node settings",

  // User permissions (admin only)
  "user.view": "View user details",
  "user.create": "Create users",
  "user.delete": "Delete users",
  "user.suspend": "Suspend users",
  "user.servers": "Manage user servers",

  // Platform permissions (super admin only)
  "platform.settings": "Modify platform settings",
  "platform.blueprints": "Manage blueprints",
  "platform.billing": "Access billing",
} as const;

Permission Check

// middleware/permission.ts
export const requirePermission = (permission: string) => {
  return async (c, next) => {
    const user = c.get("user");
    const serverId = c.req.param("serverId");

    const hasPermission = await checkPermission(user.id, serverId, permission);

    if (!hasPermission) {
      return c.json({ error: "Forbidden" }, 403);
    }

    await next();
  };
};

// Usage
app.post("/servers/:serverId/start",
  authMiddleware,
  requirePermission("server.start"),
  startServerHandler
);

Console Token Flow

Step 1: User requests console access
────────────────────────────────────
User ──▶ GET /api/v1/servers/{serverId}/console-token

Step 2: API validates permissions
─────────────────────────────────
API checks:
  ├── Is user authenticated?
  ├── Does user own this server OR have subuser access?
  ├── Does user have "server.console" permission?
  └── Is server on an online node?

Step 3: API generates signed console token
──────────────────────────────────────────
API ──▶ {
          "consoleToken": "strk_console_xxxxx",
          "nodeHost": "node-us-west.stellarstack.app",
          "nodePort": 5000,
          "expiresAt": "2024-12-09T12:05:00Z",
          "permissions": {
            "canRead": true,
            "canWrite": true
          }
        }

Step 4: Frontend connects directly to daemon
────────────────────────────────────────────
Frontend ──▶ WebSocket: wss://node.example.com:5000/console
             Headers: { "Authorization": "Bearer strk_console_xxxxx" }

Step 5: Daemon validates token
──────────────────────────────
Daemon:
  ├── Verifies JWT signature
  ├── Checks token expiration
  ├── Checks serverId matches container
  └── Extracts permissions

Console Token Structure

interface ConsoleTokenPayload {
  // Standard JWT claims
  iss: "stellarstack";
  sub: string;        // User ID
  aud: string;        // Node ID
  exp: number;        // Expiration
  iat: number;        // Issued at
  jti: string;        // Unique token ID

  // Custom claims
  serverId: string;
  containerId: string;
  permissions: {
    canRead: boolean;
    canWrite: boolean;
  };
  metadata: {
    username: string;
    ip: string;
  };
}

Daemon Authentication

Registration Flow

  1. Admin generates registration token in panel
  2. Daemon uses token to register with control plane
  3. Daemon receives long-lived API token and certificate
  4. Daemon connects to Redis with token authentication

Token Types

Token TypeFormatPurposeLifetime
Registrationstrk_reg_xxxxxOne-time node registration24 hours
Node APIstrk_node_xxxxxDaemon ↔ Redis communication1 year (rotatable)
Consolestrk_console_xxxxxUser ↔ Daemon WebSocket5 minutes