refactor: drop PAM auth and require password for funnel

This commit is contained in:
Peter Steinberger
2025-12-23 13:13:09 +00:00
parent cd6ed79433
commit c8c807adcc
22 changed files with 47 additions and 278 deletions

View File

@@ -1,29 +1,23 @@
import { timingSafeEqual } from "node:crypto";
import type { IncomingMessage } from "node:http";
import os from "node:os";
import { type PamAvailability, verifyPamCredentials } from "../infra/pam.js";
export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "system";
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
export type ResolvedGatewayAuth = {
mode: ResolvedGatewayAuthMode;
token?: string;
password?: string;
username?: string;
allowTailscale: boolean;
};
export type GatewayAuthResult = {
ok: boolean;
method?: "none" | "token" | "password" | "system" | "tailscale";
method?: "none" | "token" | "password" | "tailscale";
user?: string;
reason?: string;
};
type ConnectAuth = {
token?: string;
username?: string;
password?: string;
};
@@ -104,10 +98,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
);
}
export function assertGatewayAuthConfigured(
auth: ResolvedGatewayAuth,
pam: PamAvailability,
): void {
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
if (auth.mode === "token" && !auth.token) {
throw new Error(
"gateway auth mode is token, but CLAWDIS_GATEWAY_TOKEN is not set",
@@ -118,13 +109,6 @@ export function assertGatewayAuthConfigured(
"gateway auth mode is password, but no password was configured",
);
}
if (auth.mode === "system" && !pam.available) {
throw new Error(
`gateway auth mode is system, but PAM auth is unavailable${
pam.error ? `: ${pam.error}` : ""
}`,
);
}
}
export async function authorizeGatewayConnect(params: {
@@ -170,21 +154,6 @@ export async function authorizeGatewayConnect(params: {
return { ok: true, method: "password" };
}
if (auth.mode === "system") {
const password = connectAuth?.password;
if (!password) return { ok: false, reason: "unauthorized" };
const username = (
connectAuth?.username ??
auth.username ??
os.userInfo().username
).trim();
if (!username) return { ok: false, reason: "unauthorized" };
const ok = await verifyPamCredentials(username, password);
return ok
? { ok: true, method: "system", user: username }
: { ok: false, reason: "unauthorized" };
}
if (auth.allowTailscale) {
const tailscaleUser = getTailscaleUser(req);
if (tailscaleUser && isTailscaleProxyRequest(req)) {

View File

@@ -5,7 +5,6 @@ import { PROTOCOL_VERSION } from "./protocol/index.js";
export type CallGatewayOptions = {
url?: string;
token?: string;
username?: string;
password?: string;
method: string;
params?: unknown;
@@ -37,7 +36,6 @@ export async function callGateway<T = unknown>(
const client = new GatewayClient({
url: opts.url,
token: opts.token,
username: opts.username,
password: opts.password,
instanceId: opts.instanceId ?? randomUUID(),
clientName: opts.clientName ?? "cli",

View File

@@ -20,7 +20,6 @@ type Pending = {
export type GatewayClientOptions = {
url?: string; // ws://127.0.0.1:18789
token?: string;
username?: string;
password?: string;
instanceId?: string;
clientName?: string;
@@ -86,10 +85,9 @@ export class GatewayClient {
private sendConnect() {
const auth =
this.opts.token || this.opts.password || this.opts.username
this.opts.token || this.opts.password
? {
token: this.opts.token,
username: this.opts.username,
password: this.opts.password,
}
: undefined;

View File

@@ -77,7 +77,6 @@ export const ConnectParamsSchema = Type.Object(
Type.Object(
{
token: Type.Optional(Type.String()),
username: Type.Optional(Type.String()),
password: Type.Optional(Type.String()),
},
{ additionalProperties: false },

View File

@@ -338,7 +338,6 @@ async function connectReq(
ws: WebSocket,
opts?: {
token?: string;
username?: string;
password?: string;
minProtocol?: number;
maxProtocol?: number;
@@ -368,10 +367,9 @@ async function connectReq(
},
caps: [],
auth:
opts?.token || opts?.password || opts?.username
opts?.token || opts?.password
? {
token: opts?.token,
username: opts?.username,
password: opts?.password,
}
: undefined,

View File

@@ -76,7 +76,6 @@ import {
requestNodePairing,
verifyNodeToken,
} from "../infra/node-pairing.js";
import { getPamAvailability } from "../infra/pam.js";
import { ensureClawdisCliOnPath } from "../infra/path-env.js";
import {
enqueueSystemEvent,
@@ -1211,30 +1210,24 @@ export async function startGatewayServer(
const token = getGatewayToken();
const password =
authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined;
const username =
authConfig.username ?? process.env.CLAWDIS_GATEWAY_USERNAME ?? undefined;
const authMode: ResolvedGatewayAuth["mode"] =
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
const allowTailscale =
authConfig.allowTailscale ??
(tailscaleMode === "serve" &&
authMode !== "password" &&
authMode !== "system");
(tailscaleMode === "serve" && authMode !== "password");
const resolvedAuth: ResolvedGatewayAuth = {
mode: authMode,
token,
password,
username,
allowTailscale,
};
const canvasHostEnabled =
process.env.CLAWDIS_SKIP_CANVAS_HOST !== "1" &&
cfgAtStart.canvasHost?.enabled !== false;
const pamAvailability = await getPamAvailability();
assertGatewayAuthConfigured(resolvedAuth, pamAvailability);
if (tailscaleMode === "funnel" && authMode === "none") {
assertGatewayAuthConfigured(resolvedAuth);
if (tailscaleMode === "funnel" && authMode !== "password") {
throw new Error(
"tailscale funnel requires gateway auth (set gateway.auth or CLAWDIS_GATEWAY_TOKEN)",
"tailscale funnel requires gateway auth mode=password (set gateway.auth.password or CLAWDIS_GATEWAY_PASSWORD)",
);
}
if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) {