Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InMemoryControlPlaneStore, type ControlPlaneStore } from "./persistence
import type { EdgeMeshEvent, EdgeMeshPlugin } from "./plugins/types.js";
import { createTelemetryPlugin, type TelemetryPlugin } from "./plugins/telemetry-plugin.js";
import { JobTokenManager, NodeJwtManager, NodeTrustManager } from "./security.js";
import { requireSecretInProduction } from "./utils/secrets.js";
import { computeRetryDecision } from "./control/retry-policy.js";
import { startTimeoutReaper } from "./control/timeout-reaper.js";

Expand Down Expand Up @@ -50,7 +51,7 @@ export function buildControlPlane(
const tokenManager = new JobTokenManager();
const trustManager = new NodeTrustManager();
const nodeJwtManager = options.nodeJwtManager ?? new NodeJwtManager();
const adminSecret = process.env.EDGEMESH_ADMIN_SECRET ?? "admin-dev";
const adminSecret = requireSecretInProduction("EDGEMESH_ADMIN_SECRET", process.env.EDGEMESH_ADMIN_SECRET, "admin-dev");

const events: EdgeMeshEvent[] = [];
const ctx = {
Expand Down
19 changes: 16 additions & 3 deletions src/security.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { requireSecretInProduction } from "./utils/secrets.js";

export type JobTokenPayload = {
jobId: string;
Expand All @@ -21,7 +22,11 @@ export class JobTokenManager {
private usedJtiExp = new Map<string, number>();

constructor(secret?: string) {
this.secret = secret ?? process.env.EDGEMESH_JOB_TOKEN_SECRET ?? "dev-secret";
this.secret = requireSecretInProduction(
"EDGEMESH_JOB_TOKEN_SECRET",
secret ?? process.env.EDGEMESH_JOB_TOKEN_SECRET,
"dev-secret"
);
}

issue(input: Omit<JobTokenPayload, "jti">) {
Expand Down Expand Up @@ -90,7 +95,11 @@ export class NodeJwtManager {
private readonly ttlMs: number;

constructor(secret?: string, ttlMs?: number) {
this.secret = secret ?? process.env.EDGEMESH_NODE_JWT_SECRET ?? "node-jwt-dev";
this.secret = requireSecretInProduction(
"EDGEMESH_NODE_JWT_SECRET",
secret ?? process.env.EDGEMESH_NODE_JWT_SECRET,
"node-jwt-dev"
);
this.ttlMs = ttlMs ?? 24 * 60 * 60 * 1000; // 24 h default
}

Expand Down Expand Up @@ -140,7 +149,11 @@ export class NodeTrustManager {
private bootstrapSecret: string;

constructor(secret?: string) {
this.bootstrapSecret = secret ?? process.env.EDGEMESH_BOOTSTRAP_SECRET ?? "bootstrap-dev";
this.bootstrapSecret = requireSecretInProduction(
"EDGEMESH_BOOTSTRAP_SECRET",
secret ?? process.env.EDGEMESH_BOOTSTRAP_SECRET,
"bootstrap-dev"
);
}

verifyBootstrapToken(token?: string): boolean {
Expand Down
23 changes: 23 additions & 0 deletions src/utils/secrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Validates that a secret is provided in production environments.
* Falls back to a development default only in non-production.
*
* @param secretName - Name of the secret for error messages
* @param envVar - Value from environment variable (may be undefined)
* @param devDefault - Development fallback value
* @returns The secret value (either from env or dev default)
* @throws Error if secret is missing in production
*/
export function requireSecretInProduction(
secretName: string,
envVar: string | undefined,
devDefault: string
): string {
if (!envVar && process.env.NODE_ENV === 'production') {
throw new Error(
`${secretName} must be set in production. ` +
`Set the environment variable or NODE_ENV to non-production for development.`
);
}
return envVar ?? devDefault;
}
Loading