refactor: drop PAM auth and require password for funnel
This commit is contained in:
@@ -18,7 +18,6 @@ import { forceFreePortAndWait } from "./ports.js";
|
||||
type GatewayRpcOpts = {
|
||||
url?: string;
|
||||
token?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
timeout?: string;
|
||||
expectFinal?: boolean;
|
||||
@@ -30,8 +29,7 @@ const gatewayCallOpts = (cmd: Command) =>
|
||||
cmd
|
||||
.option("--url <url>", "Gateway WebSocket URL", "ws://127.0.0.1:18789")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--username <username>", "Gateway username (system auth)")
|
||||
.option("--password <password>", "Gateway password (password/system auth)")
|
||||
.option("--password <password>", "Gateway password (password auth)")
|
||||
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||
.option("--expect-final", "Wait for final response (agent)", false);
|
||||
|
||||
@@ -43,7 +41,6 @@ const callGatewayCli = async (
|
||||
callGateway({
|
||||
url: opts.url,
|
||||
token: opts.token,
|
||||
username: opts.username,
|
||||
password: opts.password,
|
||||
method,
|
||||
params,
|
||||
@@ -66,9 +63,8 @@ export function registerGatewayCli(program: Command) {
|
||||
"--token <token>",
|
||||
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
||||
)
|
||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password"|"system")')
|
||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
|
||||
.option("--password <password>", "Password for auth mode=password")
|
||||
.option("--username <username>", "Default username for system auth")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
||||
@@ -121,13 +117,12 @@ export function registerGatewayCli(program: Command) {
|
||||
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
||||
const authMode =
|
||||
authModeRaw === "token" ||
|
||||
authModeRaw === "password" ||
|
||||
authModeRaw === "system"
|
||||
authModeRaw === "password"
|
||||
? authModeRaw
|
||||
: null;
|
||||
if (authModeRaw && !authMode) {
|
||||
defaultRuntime.error(
|
||||
'Invalid --auth (use "token", "password", or "system")',
|
||||
'Invalid --auth (use "token" or "password")',
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
@@ -205,11 +200,10 @@ export function registerGatewayCli(program: Command) {
|
||||
server = await startGatewayServer(port, {
|
||||
bind,
|
||||
auth:
|
||||
authMode || opts.password || opts.username || authModeRaw
|
||||
authMode || opts.password || authModeRaw
|
||||
? {
|
||||
mode: authMode ?? undefined,
|
||||
password: opts.password ? String(opts.password) : undefined,
|
||||
username: opts.username ? String(opts.username) : undefined,
|
||||
}
|
||||
: undefined,
|
||||
tailscale:
|
||||
@@ -245,9 +239,8 @@ export function registerGatewayCli(program: Command) {
|
||||
"--token <token>",
|
||||
"Shared token required in connect.params.auth.token (default: CLAWDIS_GATEWAY_TOKEN env if set)",
|
||||
)
|
||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password"|"system")')
|
||||
.option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
|
||||
.option("--password <password>", "Password for auth mode=password")
|
||||
.option("--username <username>", "Default username for system auth")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
'Tailscale exposure mode ("off"|"serve"|"funnel")',
|
||||
@@ -342,13 +335,12 @@ export function registerGatewayCli(program: Command) {
|
||||
const authModeRaw = opts.auth ? String(opts.auth) : undefined;
|
||||
const authMode =
|
||||
authModeRaw === "token" ||
|
||||
authModeRaw === "password" ||
|
||||
authModeRaw === "system"
|
||||
authModeRaw === "password"
|
||||
? authModeRaw
|
||||
: null;
|
||||
if (authModeRaw && !authMode) {
|
||||
defaultRuntime.error(
|
||||
'Invalid --auth (use "token", "password", or "system")',
|
||||
'Invalid --auth (use "token" or "password")',
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
@@ -443,11 +435,10 @@ export function registerGatewayCli(program: Command) {
|
||||
server = await startGatewayServer(port, {
|
||||
bind,
|
||||
auth:
|
||||
authMode || opts.password || opts.username || authModeRaw
|
||||
authMode || opts.password || authModeRaw
|
||||
? {
|
||||
mode: authMode ?? undefined,
|
||||
password: opts.password ? String(opts.password) : undefined,
|
||||
username: opts.username ? String(opts.username) : undefined,
|
||||
}
|
||||
: undefined,
|
||||
tailscale:
|
||||
|
||||
@@ -115,13 +115,11 @@ export type GatewayControlUiConfig = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type GatewayAuthMode = "token" | "password" | "system";
|
||||
export type GatewayAuthMode = "token" | "password";
|
||||
|
||||
export type GatewayAuthConfig = {
|
||||
/** Authentication mode for Gateway connections. Defaults to token when set. */
|
||||
mode?: GatewayAuthMode;
|
||||
/** Username for system auth (PAM). Defaults to current user. */
|
||||
username?: string;
|
||||
/** Shared password for password mode (consider env instead). */
|
||||
password?: string;
|
||||
/** Allow Tailscale identity headers when serve mode is enabled. */
|
||||
@@ -522,10 +520,8 @@ const ClawdisSchema = z.object({
|
||||
.union([
|
||||
z.literal("token"),
|
||||
z.literal("password"),
|
||||
z.literal("system"),
|
||||
])
|
||||
.optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
allowTailscale: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
type PamAuthenticate = (
|
||||
username: string,
|
||||
password: string,
|
||||
callback: (err: Error | null) => void,
|
||||
) => void;
|
||||
|
||||
let pamAuth: PamAuthenticate | null | undefined;
|
||||
let pamError: string | null = null;
|
||||
|
||||
async function loadPam(): Promise<void> {
|
||||
if (pamAuth !== undefined) return;
|
||||
try {
|
||||
// Vite/Vitest: avoid static analysis/bundling for optional native deps.
|
||||
const pkgName = "authenticate-pam";
|
||||
const mod = (await import(pkgName)) as
|
||||
| { authenticate?: PamAuthenticate; default?: PamAuthenticate }
|
||||
| PamAuthenticate;
|
||||
const candidate =
|
||||
typeof mod === "function"
|
||||
? mod
|
||||
: typeof (mod as { authenticate?: PamAuthenticate }).authenticate ===
|
||||
"function"
|
||||
? (mod as { authenticate: PamAuthenticate }).authenticate
|
||||
: typeof (mod as { default?: PamAuthenticate }).default === "function"
|
||||
? (mod as { default: PamAuthenticate }).default
|
||||
: null;
|
||||
if (!candidate) {
|
||||
throw new Error(
|
||||
"authenticate-pam did not export an authenticate function",
|
||||
);
|
||||
}
|
||||
pamAuth = candidate;
|
||||
} catch (err) {
|
||||
pamAuth = null;
|
||||
pamError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
export type PamAvailability = {
|
||||
available: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function getPamAvailability(): Promise<PamAvailability> {
|
||||
await loadPam();
|
||||
return pamAuth
|
||||
? { available: true }
|
||||
: { available: false, error: pamError ?? undefined };
|
||||
}
|
||||
|
||||
export async function verifyPamCredentials(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
await loadPam();
|
||||
const auth = pamAuth;
|
||||
if (!auth) return false;
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
auth(username, password, (err) => resolve(!err));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user