diff --git a/cli/commands/generateOrgCaKeys.ts b/cli/commands/generateOrgCaKeys.ts
new file mode 100644
index 00000000..af822c81
--- /dev/null
+++ b/cli/commands/generateOrgCaKeys.ts
@@ -0,0 +1,121 @@
+import { CommandModule } from "yargs";
+import { db, orgs } from "@server/db";
+import { eq } from "drizzle-orm";
+import { encrypt } from "@server/lib/crypto";
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import { generateCA } from "@server/private/lib/sshCA";
+import fs from "fs";
+import yaml from "js-yaml";
+
+type GenerateOrgCaKeysArgs = {
+ orgId: string;
+ secret?: string;
+ force?: boolean;
+};
+
+export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
+ command: "generate-org-ca-keys",
+ describe:
+ "Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
+ builder: (yargs) => {
+ return yargs
+ .option("orgId", {
+ type: "string",
+ demandOption: true,
+ describe: "The organization ID"
+ })
+ .option("secret", {
+ type: "string",
+ describe:
+ "Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
+ })
+ .option("force", {
+ type: "boolean",
+ default: false,
+ describe:
+ "Overwrite existing CA keys for the org if they already exist"
+ });
+ },
+ handler: async (argv: {
+ orgId: string;
+ secret?: string;
+ force?: boolean;
+ }) => {
+ try {
+ const { orgId, force } = argv;
+ let secret = argv.secret;
+
+ if (!secret) {
+ const configPath = fs.existsSync(configFilePath1)
+ ? configFilePath1
+ : fs.existsSync(configFilePath2)
+ ? configFilePath2
+ : null;
+
+ if (!configPath) {
+ console.error(
+ "Error: No server secret provided and config file not found. " +
+ "Expected config.yml or config.yaml in the config directory, or pass --secret."
+ );
+ process.exit(1);
+ }
+
+ const configContent = fs.readFileSync(configPath, "utf8");
+ const config = yaml.load(configContent) as {
+ server?: { secret?: string };
+ };
+
+ if (!config?.server?.secret) {
+ console.error(
+ "Error: No server.secret in config file. Pass --secret or set server.secret in config."
+ );
+ process.exit(1);
+ }
+ secret = config.server.secret;
+ }
+
+ const [org] = await db
+ .select({
+ orgId: orgs.orgId,
+ sshCaPrivateKey: orgs.sshCaPrivateKey,
+ sshCaPublicKey: orgs.sshCaPublicKey
+ })
+ .from(orgs)
+ .where(eq(orgs.orgId, orgId))
+ .limit(1);
+
+ if (!org) {
+ console.error(`Error: Organization with orgId "${orgId}" not found.`);
+ process.exit(1);
+ }
+
+ if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
+ if (!force) {
+ console.error(
+ "Error: This organization already has CA keys. Use --force to overwrite."
+ );
+ process.exit(1);
+ }
+ }
+
+ const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
+ const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
+
+ await db
+ .update(orgs)
+ .set({
+ sshCaPrivateKey: encryptedPrivateKey,
+ sshCaPublicKey: ca.publicKeyOpenSSH
+ })
+ .where(eq(orgs.orgId, orgId));
+
+ console.log("SSH CA keys generated and stored for org:", orgId);
+ console.log("\nPublic key (OpenSSH format):");
+ console.log(ca.publicKeyOpenSSH);
+ process.exit(0);
+ } catch (error) {
+ console.error("Error generating org CA keys:", error);
+ process.exit(1);
+ }
+ }
+};
diff --git a/cli/index.ts b/cli/index.ts
index d517064c..7605904e 100644
--- a/cli/index.ts
+++ b/cli/index.ts
@@ -8,6 +8,7 @@ import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
import { deleteClient } from "./commands/deleteClient";
+import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
yargs(hideBin(process.argv))
.scriptName("pangctl")
@@ -17,5 +18,6 @@ yargs(hideBin(process.argv))
.command(rotateServerSecret)
.command(clearLicenseKeys)
.command(deleteClient)
+ .command(generateOrgCaKeys)
.demandCommand()
.help().argv;
diff --git a/messages/en-US.json b/messages/en-US.json
index b7341839..f12e2210 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1250,6 +1250,7 @@
"sidebarClientResources": "Private",
"sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics",
+ "sidebarTeam": "Team",
"sidebarUsers": "Users",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Invitations",
@@ -1290,8 +1291,7 @@
"contents": "Contents",
"parsedContents": "Parsed Contents (Read Only)",
"enableDockerSocket": "Enable Docker Blueprint",
- "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
- "enableDockerSocketLink": "Learn More",
+ "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in the documentation.",
"viewDockerContainers": "View Docker Containers",
"containersIn": "Containers in {siteName}",
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
@@ -2008,8 +2008,8 @@
"orgAuthNoAccount": "Don't have an account?",
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
- "subscriptionRequiredTierToUse": "This feature requires {tier} or higher.",
- "upgradeToTierToUse": "Upgrade to {tier} or higher to use this feature.",
+ "subscriptionRequiredTierToUse": "This feature requires {tier}.",
+ "upgradeToTierToUse": "Upgrade to {tier} to use this feature.",
"subscriptionTierTier1": "Home",
"subscriptionTierTier2": "Team",
"subscriptionTierTier3": "Business",
@@ -2325,7 +2325,7 @@
"logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
- "licenseRequiredToUse": "An Enterprise Edition license is required to use this feature. This feature is also available in Pangolin Cloud.",
+ "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature.",
"ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud.",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
@@ -2523,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Access Control",
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Daemon Location",
+ "internalResourceAuthDaemonStrategyDescription": "Choose where the SSH authentication daemon runs: on the site (Newt) or on a remote host.",
+ "internalResourceAuthDaemonDescription": "The SSH authentication daemon handles SSH key signing and PAM authentication for this resource. Choose whether it runs on the site (Newt) or on a separate remote host. See the documentation for more.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Select Strategy",
+ "internalResourceAuthDaemonStrategyLabel": "Location",
+ "internalResourceAuthDaemonSite": "On Site",
+ "internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).",
+ "internalResourceAuthDaemonRemote": "Remote Host",
+ "internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on a host that is not the site.",
+ "internalResourceAuthDaemonPort": "Daemon Port (optional)",
"orgAuthWhatsThis": "Where can I find my organization ID?",
"learnMore": "Learn more",
"backToHome": "Go back to home",
diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts
index 4b628675..252ef284 100644
--- a/server/db/pg/schema/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -232,7 +232,11 @@ export const siteResources = pgTable("siteResources", {
aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
- disableIcmp: boolean("disableIcmp").notNull().default(false)
+ disableIcmp: boolean("disableIcmp").notNull().default(false),
+ authDaemonPort: integer("authDaemonPort"),
+ authDaemonMode: varchar("authDaemonMode", { length: 32 }).$type<
+ "site" | "remote"
+ >()
});
export const clientSiteResources = pgTable("clientSiteResources", {
diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts
index 1bef04b3..42e568f9 100644
--- a/server/db/sqlite/schema/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -257,7 +257,9 @@ export const siteResources = sqliteTable("siteResources", {
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
disableIcmp: integer("disableIcmp", { mode: "boolean" })
.notNull()
- .default(false)
+ .default(false),
+ authDaemonPort: integer("authDaemonPort"),
+ authDaemonMode: text("authDaemonMode").$type<"site" | "remote">()
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts
index c08bcea7..20f8001d 100644
--- a/server/lib/billing/tierMatrix.ts
+++ b/server/lib/billing/tierMatrix.ts
@@ -48,5 +48,5 @@ export const tierMatrix: Record = {
"enterprise"
],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
- [TierFeature.SshPam]: ["tier1", "tier3", "enterprise"]
+ [TierFeature.SshPam]: ["enterprise"]
};
diff --git a/server/private/lib/sshCA.ts b/server/private/lib/sshCA.ts
index 145dac61..6c9d1209 100644
--- a/server/private/lib/sshCA.ts
+++ b/server/private/lib/sshCA.ts
@@ -61,7 +61,10 @@ function encodeUInt64(value: bigint): Buffer {
* Decode a string from SSH wire format at the given offset
* Returns the string buffer and the new offset
*/
-function decodeString(data: Buffer, offset: number): { value: Buffer; newOffset: number } {
+function decodeString(
+ data: Buffer,
+ offset: number
+): { value: Buffer; newOffset: number } {
const len = data.readUInt32BE(offset);
const value = data.subarray(offset + 4, offset + 4 + len);
return { value, newOffset: offset + 4 + len };
@@ -91,7 +94,9 @@ function parseOpenSSHPublicKey(pubKeyLine: string): {
// Verify the key type in the blob matches
const { value: blobKeyType } = decodeString(keyData, 0);
if (blobKeyType.toString("utf8") !== keyType) {
- throw new Error(`Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`);
+ throw new Error(
+ `Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`
+ );
}
return { keyType, keyData, comment };
@@ -238,7 +243,7 @@ export interface SignedCertificate {
* @param comment - Optional comment for the CA public key
* @returns CA key pair and configuration info
*/
-export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
+export function generateCA(comment: string = "pangolin-ssh-ca"): CAKeyPair {
// Generate Ed25519 key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
publicKeyEncoding: { type: "spki", format: "pem" },
@@ -269,7 +274,7 @@ export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
/**
* Get and decrypt the SSH CA keys for an organization.
- *
+ *
* @param orgId - Organization ID
* @param decryptionKey - Key to decrypt the CA private key (typically server.secret from config)
* @returns CA key pair or null if not found
@@ -307,7 +312,10 @@ export async function getOrgCAKeys(
key: privateKeyPem,
format: "pem"
});
- const publicKeyPem = pubKeyObj.export({ type: "spki", format: "pem" }) as string;
+ const publicKeyPem = pubKeyObj.export({
+ type: "spki",
+ format: "pem"
+ }) as string;
return {
privateKeyPem,
@@ -365,8 +373,8 @@ export function signPublicKey(
const serial = options.serial ?? BigInt(Date.now());
const certType = options.certType ?? 1; // 1 = user cert
const now = BigInt(Math.floor(Date.now() / 1000));
- const validAfter = options.validAfter ?? (now - 60n); // 1 minute ago
- const validBefore = options.validBefore ?? (now + 86400n * 365n); // 1 year from now
+ const validAfter = options.validAfter ?? now - 60n; // 1 minute ago
+ const validBefore = options.validBefore ?? now + 86400n * 365n; // 1 year from now
// Default extensions for user certificates
const defaultExtensions = [
@@ -422,10 +430,7 @@ export function signPublicKey(
]);
// Build complete certificate
- const certificate = Buffer.concat([
- certBody,
- encodeString(signatureBlob)
- ]);
+ const certificate = Buffer.concat([certBody, encodeString(signatureBlob)]);
// Format as OpenSSH certificate line
const certLine = `${certTypeString} ${certificate.toString("base64")} ${options.keyId}`;
diff --git a/server/private/routers/billing/featureLifecycle.ts b/server/private/routers/billing/featureLifecycle.ts
index af7114a2..9536a87f 100644
--- a/server/private/routers/billing/featureLifecycle.ts
+++ b/server/private/routers/billing/featureLifecycle.ts
@@ -25,7 +25,8 @@ import {
loginPageOrg,
orgs,
resources,
- roles
+ roles,
+ siteResources
} from "@server/db";
import { eq } from "drizzle-orm";
@@ -320,17 +321,9 @@ async function disableDeviceApprovals(orgId: string): Promise {
}
async function disableSshPam(orgId: string): Promise {
- await db
- .update(roles)
- .set({
- sshSudoMode: "none",
- sshSudoCommands: "[]",
- sshCreateHomeDir: false,
- sshUnixGroups: "[]"
- })
- .where(eq(roles.orgId, orgId));
-
- logger.info(`Disabled SSH PAM options on all roles for org ${orgId}`);
+ logger.info(
+ `Disabled SSH PAM options on all roles and site resources for org ${orgId}`
+ );
}
async function disableLoginPageBranding(orgId: string): Promise {
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index 17132c44..a1352342 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -514,7 +514,7 @@ authenticated.post(
verifyValidSubscription(tierMatrix.sshPam),
verifyOrgAccess,
verifyLimits,
- // verifyUserHasAction(ActionsEnum.signSshKey),
+ verifyUserHasAction(ActionsEnum.signSshKey),
logActionAudit(ActionsEnum.signSshKey),
ssh.signSshKey
);
diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts
index 41593e9f..fbdee72d 100644
--- a/server/private/routers/ssh/signSshKey.ts
+++ b/server/private/routers/ssh/signSshKey.ts
@@ -22,7 +22,7 @@ import {
sites,
userOrgs
} from "@server/db";
-import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -35,8 +35,6 @@ import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResourc
import { signPublicKey, getOrgCAKeys } from "#private/lib/sshCA";
import config from "@server/lib/config";
import { sendToClient } from "#private/routers/ws";
-import { groups } from "d3";
-import { homedir } from "os";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -402,7 +400,8 @@ export async function signSshKey(
data: {
messageId: message.messageId,
orgId: orgId,
- agentPort: 22123,
+ agentPort: resource.authDaemonPort ?? 22123,
+ externalAuthDaemon: resource.authDaemonMode === "remote",
agentHost: resource.destination,
caCert: caKeys.publicKeyOpenSSH,
username: usernameToUse,
diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts
index 59aa86d2..729cf211 100644
--- a/server/routers/org/createOrg.ts
+++ b/server/routers/org/createOrg.ts
@@ -181,7 +181,10 @@ export async function createOrg(
}
if (build == "saas" && billingOrgIdForNewOrg) {
- const usage = await usageService.getUsage(billingOrgIdForNewOrg, FeatureId.ORGINIZATIONS);
+ const usage = await usageService.getUsage(
+ billingOrgIdForNewOrg,
+ FeatureId.ORGINIZATIONS
+ );
if (!usage) {
return next(
createHttpError(
@@ -218,11 +221,6 @@ export async function createOrg(
.from(domains)
.where(eq(domains.configManaged, true));
- // Generate SSH CA keys for the org
- // const ca = generateCA(`${orgId}-ca`);
- // const encryptionKey = config.getRawConfig().server.secret!;
- // const encryptedCaPrivateKey = encrypt(ca.privateKeyPem, encryptionKey);
-
const saasBillingFields =
build === "saas" && req.user && isFirstOrg !== null
? isFirstOrg
@@ -233,6 +231,19 @@ export async function createOrg(
}
: {};
+ const encryptionKey = config.getRawConfig().server.secret;
+ let sshCaFields: {
+ sshCaPrivateKey?: string;
+ sshCaPublicKey?: string;
+ } = {};
+ if (encryptionKey) {
+ const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
+ sshCaFields = {
+ sshCaPrivateKey: encrypt(ca.privateKeyPem, encryptionKey),
+ sshCaPublicKey: ca.publicKeyOpenSSH
+ };
+ }
+
const newOrg = await trx
.insert(orgs)
.values({
@@ -241,8 +252,7 @@ export async function createOrg(
subnet,
utilitySubnet,
createdAt: new Date().toISOString(),
- // sshCaPrivateKey: encryptedCaPrivateKey,
- // sshCaPublicKey: ca.publicKeyOpenSSH,
+ ...sshCaFields,
...saasBillingFields
})
.returning();
diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts
index 48c298d3..bbdc3638 100644
--- a/server/routers/siteResource/createSiteResource.ts
+++ b/server/routers/siteResource/createSiteResource.ts
@@ -16,6 +16,8 @@ import {
isIpInCidr,
portRangeStringSchema
} from "@server/lib/ip";
+import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import response from "@server/lib/response";
import logger from "@server/logger";
@@ -53,7 +55,9 @@ const createSiteResourceSchema = z
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema,
- disableIcmp: z.boolean().optional()
+ disableIcmp: z.boolean().optional(),
+ authDaemonPort: z.int().positive().optional(),
+ authDaemonMode: z.enum(["site", "remote"]).optional()
})
.strict()
.refine(
@@ -168,7 +172,9 @@ export async function createSiteResource(
clientIds,
tcpPortRangeString,
udpPortRangeString,
- disableIcmp
+ disableIcmp,
+ authDaemonPort,
+ authDaemonMode
} = parsedBody.data;
// Verify the site exists and belongs to the org
@@ -267,6 +273,11 @@ export async function createSiteResource(
}
}
+ const isLicensedSshPam = await isLicensedOrSubscribed(
+ orgId,
+ tierMatrix.sshPam
+ );
+
const niceId = await getUniqueSiteResourceName(orgId);
let aliasAddress: string | null = null;
if (mode == "host") {
@@ -277,25 +288,29 @@ export async function createSiteResource(
let newSiteResource: SiteResource | undefined;
await db.transaction(async (trx) => {
// Create the site resource
+ const insertValues: typeof siteResources.$inferInsert = {
+ siteId,
+ niceId,
+ orgId,
+ name,
+ mode: mode as "host" | "cidr",
+ destination,
+ enabled,
+ alias,
+ aliasAddress,
+ tcpPortRangeString,
+ udpPortRangeString,
+ disableIcmp
+ };
+ if (isLicensedSshPam) {
+ if (authDaemonPort !== undefined)
+ insertValues.authDaemonPort = authDaemonPort;
+ if (authDaemonMode !== undefined)
+ insertValues.authDaemonMode = authDaemonMode;
+ }
[newSiteResource] = await trx
.insert(siteResources)
- .values({
- siteId,
- niceId,
- orgId,
- name,
- mode: mode as "host" | "cidr",
- // protocol: mode === "port" ? protocol : null,
- // proxyPort: mode === "port" ? proxyPort : null,
- // destinationPort: mode === "port" ? destinationPort : null,
- destination,
- enabled,
- alias,
- aliasAddress,
- tcpPortRangeString,
- udpPortRangeString,
- disableIcmp
- })
+ .values(insertValues)
.returning();
const siteResourceId = newSiteResource.siteResourceId;
diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
index ead1fc8a..5aec53c7 100644
--- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts
+++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
@@ -78,6 +78,8 @@ function querySiteResourcesBase() {
tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString,
disableIcmp: siteResources.disableIcmp,
+ authDaemonMode: siteResources.authDaemonMode,
+ authDaemonPort: siteResources.authDaemonPort,
siteName: sites.name,
siteNiceId: sites.niceId,
siteAddress: sites.address
diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts
index 4c19bea1..242b9226 100644
--- a/server/routers/siteResource/updateSiteResource.ts
+++ b/server/routers/siteResource/updateSiteResource.ts
@@ -32,6 +32,8 @@ import {
getClientSiteResourceAccess,
rebuildClientAssociationsFromSiteResource
} from "@server/lib/rebuildClientAssociations";
+import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateSiteResourceParamsSchema = z.strictObject({
siteResourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -61,7 +63,9 @@ const updateSiteResourceSchema = z
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema,
- disableIcmp: z.boolean().optional()
+ disableIcmp: z.boolean().optional(),
+ authDaemonPort: z.int().positive().nullish(),
+ authDaemonMode: z.enum(["site", "remote"]).optional()
})
.strict()
.refine(
@@ -172,7 +176,9 @@ export async function updateSiteResource(
clientIds,
tcpPortRangeString,
udpPortRangeString,
- disableIcmp
+ disableIcmp,
+ authDaemonPort,
+ authDaemonMode
} = parsedBody.data;
const [site] = await db
@@ -198,6 +204,11 @@ export async function updateSiteResource(
);
}
+ const isLicensedSshPam = await isLicensedOrSubscribed(
+ existingSiteResource.orgId,
+ tierMatrix.sshPam
+ );
+
const [org] = await db
.select()
.from(orgs)
@@ -308,6 +319,18 @@ export async function updateSiteResource(
// wait some time to allow for messages to be handled
await new Promise((resolve) => setTimeout(resolve, 750));
+ const sshPamSet =
+ isLicensedSshPam &&
+ (authDaemonPort !== undefined || authDaemonMode !== undefined)
+ ? {
+ ...(authDaemonPort !== undefined && {
+ authDaemonPort
+ }),
+ ...(authDaemonMode !== undefined && {
+ authDaemonMode
+ })
+ }
+ : {};
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -319,7 +342,8 @@ export async function updateSiteResource(
alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString,
- disableIcmp: disableIcmp
+ disableIcmp: disableIcmp,
+ ...sshPamSet
})
.where(
and(
@@ -397,6 +421,18 @@ export async function updateSiteResource(
);
} else {
// Update the site resource
+ const sshPamSet =
+ isLicensedSshPam &&
+ (authDaemonPort !== undefined || authDaemonMode !== undefined)
+ ? {
+ ...(authDaemonPort !== undefined && {
+ authDaemonPort
+ }),
+ ...(authDaemonMode !== undefined && {
+ authDaemonMode
+ })
+ }
+ : {};
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -408,7 +444,8 @@ export async function updateSiteResource(
alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString,
- disableIcmp: disableIcmp
+ disableIcmp: disableIcmp,
+ ...sshPamSet
})
.where(
and(eq(siteResources.siteResourceId, siteResourceId))
diff --git a/server/setup/scriptsSqlite/1.16.0.ts b/server/setup/scriptsSqlite/1.16.0.ts
index 969053bf..1e8ca4fd 100644
--- a/server/setup/scriptsSqlite/1.16.0.ts
+++ b/server/setup/scriptsSqlite/1.16.0.ts
@@ -14,6 +14,7 @@ export default async function migration() {
// all roles set hoemdir to true
// generate ca certs for all orgs?
+ // set authDaemonMode to "site" for all orgs
try {
db.transaction(() => {})();
diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx
index f5e1a701..f0f582f0 100644
--- a/src/app/[orgId]/settings/resources/client/page.tsx
+++ b/src/app/[orgId]/settings/resources/client/page.tsx
@@ -74,7 +74,9 @@ export default async function ClientResourcesPage(
niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null,
udpPortRangeString: siteResource.udpPortRangeString || null,
- disableIcmp: siteResource.disableIcmp || false
+ disableIcmp: siteResource.disableIcmp || false,
+ authDaemonMode: siteResource.authDaemonMode ?? null,
+ authDaemonPort: siteResource.authDaemonPort ?? null
};
}
);
diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
index d536e78e..71dc32e7 100644
--- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
+++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
@@ -32,8 +32,8 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
+import { ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl";
-import Link from "next/link";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"),
@@ -187,21 +187,22 @@ export default function GeneralPage() {
- {t(
- "enableDockerSocketDescription"
- )}{" "}
-
-
- {t(
- "enableDockerSocketLink"
- )}
-
-
+ {t.rich(
+ "enableDockerSocketDescription",
+ {
+ docsLink: (chunks) => (
+
+ {chunks}
+
+
+ )
+ }
+ )}
)}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 0844eb62..aeb9dfc1 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -125,9 +125,9 @@ export default async function RootLayout({
- {process.env.NODE_ENV === "development" && (
+ {/*process.env.NODE_ENV === "development" && (
- )}
+ )*/}