Add new ssh config for private resources

This commit is contained in:
Owen 2026-05-26 17:50:46 -07:00
parent eca87b66f0
commit aa7004b2ff
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
22 changed files with 399 additions and 144 deletions

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Метод",
"editInternalResourceDialogEnableSsl": "Активирайте TLS",
"editInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Метод",
"createInternalResourceDialogScheme": "Метод",
"createInternalResourceDialogEnableSsl": "Активирайте TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schéma",
"editInternalResourceDialogEnableSsl": "Povolit SSL",
"editInternalResourceDialogEnableSslDescription": "Povolit šifrování SSL/TLS pro zabezpečené HTTPS připojení k cíli.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schéma",
"createInternalResourceDialogScheme": "Schéma",
"createInternalResourceDialogEnableSsl": "Povolit SSL",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS aktivieren",
"editInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schema",
"createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS aktivieren",

View file

@ -2110,6 +2110,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Scheme",
"editInternalResourceDialogEnableSsl": "Enable TLS",
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
@ -2159,6 +2160,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Scheme",
"createInternalResourceDialogScheme": "Scheme",
"createInternalResourceDialogEnableSsl": "Enable TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Activar TLS",
"editInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Activar TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Méthode HTTP",
"editInternalResourceDialogEnableSsl": "Activer TLS",
"editInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Méthode HTTP",
"createInternalResourceDialogScheme": "Méthode HTTP",
"createInternalResourceDialogEnableSsl": "Activer TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Metodo HTTP",
"editInternalResourceDialogEnableSsl": "Abilitare TLS",
"editInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Metodo HTTP",
"createInternalResourceDialogScheme": "Metodo HTTP",
"createInternalResourceDialogEnableSsl": "Abilitare TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "스킴",
"editInternalResourceDialogEnableSsl": "TLS 활성화",
"editInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "스킴",
"createInternalResourceDialogScheme": "스킴",
"createInternalResourceDialogEnableSsl": "TLS 활성화",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Skjema",
"editInternalResourceDialogEnableSsl": "Aktiver TLS",
"editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Skjema",
"createInternalResourceDialogScheme": "Skjema",
"createInternalResourceDialogEnableSsl": "Aktiver TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS inschakelen",
"editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schema",
"createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS inschakelen",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schemat",
"editInternalResourceDialogEnableSsl": "Włącz TLS",
"editInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schemat",
"createInternalResourceDialogScheme": "Schemat",
"createInternalResourceDialogEnableSsl": "Włącz TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Ativar TLS",
"editInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Ativar TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "СИДР",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Схема",
"editInternalResourceDialogEnableSsl": "Включить TLS",
"editInternalResourceDialogEnableSslDescription": "Включите шифрование SSL/TLS для защищенных HTTPS соединений с конечной точкой.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "СИДР",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Схема",
"createInternalResourceDialogScheme": "Схема",
"createInternalResourceDialogEnableSsl": "Включить TLS",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Şema",
"editInternalResourceDialogEnableSsl": "TLS Etkinleştir",
"editInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Şema",
"createInternalResourceDialogScheme": "Şema",
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",

View file

@ -2049,6 +2049,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "方案",
"editInternalResourceDialogEnableSsl": "启用 TLS",
"editInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。",
@ -2098,6 +2099,7 @@
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "方案",
"createInternalResourceDialogScheme": "方案",
"createInternalResourceDialogEnableSsl": "启用 TLS",

View file

@ -67,7 +67,7 @@ const createSiteResourceSchema = z
udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(),
authDaemonPort: z.int().positive().optional(),
authDaemonMode: z.enum(["site", "remote"]).optional(),
authDaemonMode: z.enum(["site", "remote", "native"]).optional(),
pamMode: z.enum(["passthrough", "push"]).optional(),
domainId: z.string().optional(), // only used for http mode, we need this to verify the alias is unique within the org
subdomain: z.string().optional() // only used for http mode, we need this to verify the alias is unique within the org
@ -213,6 +213,7 @@ export async function createSiteResource(
disableIcmp,
authDaemonPort,
authDaemonMode,
pamMode,
domainId,
subdomain
} = parsedBody.data;
@ -417,6 +418,7 @@ export async function createSiteResource(
insertValues.authDaemonPort = authDaemonPort;
if (authDaemonMode !== undefined)
insertValues.authDaemonMode = authDaemonMode;
if (pamMode !== undefined) insertValues.pamMode = pamMode;
}
[newSiteResource] = await trx
.insert(siteResources)

View file

@ -77,7 +77,8 @@ const updateSiteResourceSchema = z
udpPortRangeString: portRangeStringSchema,
disableIcmp: z.boolean().optional(),
authDaemonPort: z.int().positive().nullish(),
authDaemonMode: z.enum(["site", "remote"]).optional(),
authDaemonMode: z.enum(["site", "remote", "native"]).optional(),
pamMode: z.enum(["passthrough", "push"]).optional(),
domainId: z.string().optional(),
subdomain: z.string().optional()
})
@ -222,6 +223,7 @@ export async function updateSiteResource(
disableIcmp,
authDaemonPort,
authDaemonMode,
pamMode,
domainId,
subdomain
} = parsedBody.data;
@ -430,13 +432,17 @@ export async function updateSiteResource(
const sshPamSet =
isLicensedSshPam &&
(authDaemonPort !== undefined ||
authDaemonMode !== undefined)
authDaemonMode !== undefined ||
pamMode !== undefined)
? {
...(authDaemonPort !== undefined && {
authDaemonPort
}),
...(authDaemonMode !== undefined && {
authDaemonMode
}),
...(pamMode !== undefined && {
pamMode
})
}
: {};
@ -554,13 +560,17 @@ export async function updateSiteResource(
const sshPamSet =
isLicensedSshPam &&
(authDaemonPort !== undefined ||
authDaemonMode !== undefined)
authDaemonMode !== undefined ||
pamMode !== undefined)
? {
...(authDaemonPort !== undefined && {
authDaemonPort
}),
...(authDaemonMode !== undefined && {
authDaemonMode
}),
...(pamMode !== undefined && {
pamMode
})
}
: {};

View file

@ -56,7 +56,9 @@ export default async function ClientResourcesPage(
pagination = responseData.pagination;
} catch (e) {}
const siteIdParam = parsePositiveInt(searchParams.get("siteId") ?? undefined);
const siteIdParam = parsePositiveInt(
searchParams.get("siteId") ?? undefined
);
let initialFilterSite: {
siteId: number;
@ -106,7 +108,10 @@ export default async function ClientResourcesPage(
siteNiceId: siteResource.siteNiceIds[idx],
online: siteResource.siteOnlines[idx]
})),
mode: siteResource.mode,
mode:
siteResource.pamMode && siteResource.mode === "host"
? "ssh"
: siteResource.mode,
scheme: siteResource.scheme,
ssl: siteResource.ssl,
siteNames: siteResource.siteNames,
@ -125,6 +130,7 @@ export default async function ClientResourcesPage(
disableIcmp: siteResource.disableIcmp || false,
authDaemonMode: siteResource.authDaemonMode ?? null,
authDaemonPort: siteResource.authDaemonPort ?? null,
pamMode: siteResource.pamMode ?? null,
subdomain: siteResource.subdomain ?? null,
domainId: siteResource.domainId ?? null,
fullDomain: siteResource.fullDomain ?? null,

View file

@ -77,7 +77,7 @@ export type InternalResourceRow = {
siteIds: number[];
siteNiceIds: string[];
// mode: "host" | "cidr" | "port";
mode: "host" | "cidr" | "http";
mode: "host" | "cidr" | "http" | "ssh";
scheme: "http" | "https" | null;
ssl: boolean;
// protocol: string | null;
@ -90,8 +90,9 @@ export type InternalResourceRow = {
tcpPortRangeString: string | null;
udpPortRangeString: string | null;
disableIcmp: boolean;
authDaemonMode?: "site" | "remote" | null;
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
subdomain?: string | null;
domainId?: string | null;
fullDomain?: string | null;
@ -410,6 +411,10 @@ export default function ClientResourcesTable({
{
value: "http",
label: t("editInternalResourceDialogModeHttp")
},
{
value: "ssh",
label: t("editInternalResourceDialogModeSsh")
}
]}
selectedValue={searchParams.get("mode") ?? undefined}
@ -425,13 +430,14 @@ export default function ClientResourcesTable({
cell: ({ row }) => {
const resourceRow = row.original;
const modeLabels: Record<
"host" | "cidr" | "port" | "http",
"host" | "cidr" | "port" | "http" | "ssh",
string
> = {
host: t("editInternalResourceDialogModeHost"),
cidr: t("editInternalResourceDialogModeCidr"),
port: t("editInternalResourceDialogModePort"),
http: t("editInternalResourceDialogModeHttp")
http: t("editInternalResourceDialogModeHttp"),
ssh: t("editInternalResourceDialogModeSsh")
};
return <span>{modeLabels[resourceRow.mode]}</span>;
}

View file

@ -47,7 +47,9 @@ export default function CreateInternalResourceDialog({
try {
let data = { ...values };
if (
(data.mode === "host" || data.mode === "http") &&
(data.mode === "host" ||
data.mode === "http" ||
data.mode === "ssh") &&
isHostname(data.destination)
) {
const currentAlias = data.alias?.trim() || "";
@ -60,12 +62,15 @@ export default function CreateInternalResourceDialog({
}
}
// "ssh" mode maps to "host" in the backend with SSH settings
const backendMode = data.mode === "ssh" ? "host" : data.mode;
await api.put<
AxiosResponse<{ data: { siteResourceId: number } }>
>(`/org/${orgId}/site-resource`, {
name: data.name,
siteIds: data.siteIds,
mode: data.mode,
mode: backendMode,
destination: data.destination,
enabled: true,
...(data.mode === "http" && {
@ -94,7 +99,25 @@ export default function CreateInternalResourceDialog({
authDaemonPort: data.authDaemonPort
})
}),
...((data.mode === "host" || data.mode == "cidr") && {
...(data.mode === "ssh" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: undefined,
pamMode: data.pamMode ?? undefined,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" &&
data.authDaemonPort != null && {
authDaemonPort: data.authDaemonPort
})
}),
...((data.mode === "host" ||
data.mode === "ssh" ||
data.mode === "cidr") && {
tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false

View file

@ -51,7 +51,9 @@ export default function EditInternalResourceDialog({
try {
let data = { ...values };
if (
(data.mode === "host" || data.mode === "http") &&
(data.mode === "host" ||
data.mode === "http" ||
data.mode === "ssh") &&
isHostname(data.destination)
) {
const currentAlias = data.alias?.trim() || "";
@ -64,10 +66,13 @@ export default function EditInternalResourceDialog({
}
}
// "ssh" mode maps to "host" in the backend with SSH settings
const backendMode = data.mode === "ssh" ? "host" : data.mode;
await api.post(`/site-resource/${resource.id}`, {
name: data.name,
siteIds: data.siteIds,
mode: data.mode,
mode: backendMode,
niceId: data.niceId,
destination: data.destination,
...(data.mode === "http" && {
@ -95,7 +100,24 @@ export default function EditInternalResourceDialog({
authDaemonPort: data.authDaemonPort || null
})
}),
...((data.mode === "host" || data.mode === "cidr") && {
...(data.mode === "ssh" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: null,
pamMode: data.pamMode ?? undefined,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" && {
authDaemonPort: data.authDaemonPort || null
})
}),
...((data.mode === "host" ||
data.mode === "ssh" ||
data.mode === "cidr") && {
tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false

View file

@ -136,7 +136,7 @@ export const cleanForFQDN = (name: string): string =>
// --- Types ---
export type InternalResourceMode = "host" | "cidr" | "http";
export type InternalResourceMode = "host" | "cidr" | "http" | "ssh";
export type InternalResourceData = {
id: number;
@ -151,8 +151,9 @@ export type InternalResourceData = {
tcpPortRangeString?: string | null;
udpPortRangeString?: string | null;
disableIcmp?: boolean;
authDaemonMode?: "site" | "remote" | null;
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
httpHttpsPort?: number | null;
scheme?: "http" | "https" | null;
ssl?: boolean;
@ -183,8 +184,9 @@ export type InternalResourceFormValues = {
tcpPortRangeString?: string | null;
udpPortRangeString?: string | null;
disableIcmp?: boolean;
authDaemonMode?: "site" | "remote" | null;
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
httpHttpsPort?: number | null;
scheme?: "http" | "https";
ssl?: boolean;
@ -256,6 +258,10 @@ export function InternalResourceForm({
variant === "create"
? "createInternalResourceDialogModeHttp"
: "editInternalResourceDialogModeHttp";
const modeSshKey =
variant === "create"
? "createInternalResourceDialogModeSsh"
: "editInternalResourceDialogModeSsh";
const schemeLabelKey =
variant === "create"
? "createInternalResourceDialogScheme"
@ -301,7 +307,7 @@ export function InternalResourceForm({
.object({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteIds: siteIdsSchema,
mode: z.enum(["host", "cidr", "http"]),
mode: z.enum(["host", "cidr", "http", "ssh"]),
destination: z
.string()
.min(
@ -332,8 +338,12 @@ export function InternalResourceForm({
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional(),
authDaemonMode: z.enum(["site", "remote"]).optional().nullable(),
authDaemonMode: z
.enum(["site", "remote", "native"])
.optional()
.nullable(),
authDaemonPort: z.number().int().positive().optional().nullable(),
pamMode: z.enum(["passthrough", "push"]).optional().nullable(),
roles: z.array(tagSchema).optional(),
users: z.array(tagSchema).optional(),
clients: z
@ -456,6 +466,19 @@ export function InternalResourceForm({
number | null
>(null);
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
() => {
if (
variant === "edit" &&
resource &&
resource.authDaemonMode === "native"
) {
return "native";
}
return "standard";
}
);
const [tcpPortMode, setTcpPortMode] = useState<PortMode>(() =>
variant === "edit" && resource
? getPortModeFromString(resource.tcpPortRangeString)
@ -494,8 +517,12 @@ export function InternalResourceForm({
tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
authDaemonMode: resource.authDaemonMode ?? "site",
authDaemonMode:
resource.authDaemonMode === "native"
? "native"
: (resource.authDaemonMode ?? "site"),
authDaemonPort: resource.authDaemonPort ?? null,
pamMode: resource.pamMode ?? "passthrough",
httpHttpsPort: resource.httpHttpsPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
@ -524,6 +551,7 @@ export function InternalResourceForm({
disableIcmp: false,
authDaemonMode: "site",
authDaemonPort: null,
pamMode: "passthrough",
roles: [],
users: [],
clients: []
@ -546,6 +574,15 @@ export function InternalResourceForm({
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
const isHttpMode = mode === "http";
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
const pamMode = form.watch("pamMode") ?? "passthrough";
const isNative = sshServerMode === "native";
const showDaemonLocation =
mode === "ssh" && !isNative && pamMode === "push";
const showDaemonPort =
mode === "ssh" &&
!isNative &&
pamMode === "push" &&
authDaemonMode === "remote";
const hasInitialized = useRef(false);
const previousResourceId = useRef<number | null>(null);
@ -579,11 +616,13 @@ export function InternalResourceForm({
disableIcmp: false,
authDaemonMode: "site",
authDaemonPort: null,
pamMode: "passthrough",
roles: [],
users: [],
clients: []
});
setSelectedSites([]);
setSshServerMode("standard");
setTcpPortMode("all");
setUdpPortMode("all");
setTcpCustomPorts("");
@ -611,12 +650,19 @@ export function InternalResourceForm({
tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
authDaemonMode: resource.authDaemonMode ?? "site",
authDaemonMode:
resource.authDaemonMode === "native"
? "native"
: (resource.authDaemonMode ?? "site"),
authDaemonPort: resource.authDaemonPort ?? null,
pamMode: resource.pamMode ?? "passthrough",
roles: [],
users: [],
clients: []
});
setSshServerMode(
resource.authDaemonMode === "native" ? "native" : "standard"
);
setSelectedSites(buildSelectedSitesForResource(resource));
setTcpPortMode(
getPortModeFromString(resource.tcpPortRangeString)
@ -736,7 +782,7 @@ export function InternalResourceForm({
title: t("editInternalResourceDialogAccessPolicy"),
href: "#"
},
...(disableEnterpriseFeatures || mode !== "host"
...(disableEnterpriseFeatures || mode !== "ssh"
? []
: [{ title: t("sshAccess"), href: "#" }])
]}
@ -847,6 +893,12 @@ export function InternalResourceForm({
label: t(
modeHttpKey
)
},
{
value: "ssh" as const,
label: t(
modeSshKey
)
}
]
: [])
@ -1574,152 +1626,256 @@ export function InternalResourceForm({
)}
</div>
{/* SSH Access tab (host mode only) */}
{!disableEnterpriseFeatures && mode === "host" && (
{/* SSH Access tab (ssh mode only) */}
{!disableEnterpriseFeatures && mode === "ssh" && (
<div className="space-y-4 mt-4 p-1">
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
<div className="mb-8">
<label className="font-medium block">
{t("internalResourceAuthDaemonStrategy")}
</label>
<div className="text-sm text-muted-foreground">
{t.rich(
"internalResourceAuthDaemonDescription",
{/* Mode */}
<div className="space-y-3">
<p className="text-sm font-semibold">
{t("sshServerMode")}
</p>
<StrategySelect<"standard" | "native">
value={sshServerMode}
options={[
{
docsLink: (chunks) => (
<a
href={
"https://docs.pangolin.net/manage/ssh#setup-choose-your-architecture"
}
target="_blank"
rel="noopener noreferrer"
className={
"text-primary inline-flex items-center gap-1"
}
>
{chunks}
<ExternalLink className="size-3.5 shrink-0" />
</a>
)
id: "native",
title: t("sshServerModePangolin"),
description: t(
"sshServerModeNativeDescription"
),
disabled: sshSectionDisabled
},
{
id: "standard",
title: t("sshServerModeStandard"),
description: t(
"sshServerModeStandardDescription"
),
disabled: sshSectionDisabled
}
)}
</div>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="authDaemonMode"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"internalResourceAuthDaemonStrategyLabel"
)}
</FormLabel>
<FormControl>
<StrategySelect<
"site" | "remote"
>
value={
field.value ?? undefined
}
options={[
{
id: "site",
title: t(
"internalResourceAuthDaemonSite"
),
description: t(
"internalResourceAuthDaemonSiteDescription"
),
disabled:
sshSectionDisabled
},
{
id: "remote",
title: t(
"internalResourceAuthDaemonRemote"
),
description: t(
"internalResourceAuthDaemonRemoteDescription"
),
disabled:
sshSectionDisabled
}
]}
onChange={(v) => {
if (sshSectionDisabled)
return;
field.onChange(v);
if (v === "site") {
form.setValue(
"authDaemonPort",
null
);
}
}}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
]}
onChange={(v) => {
if (sshSectionDisabled) return;
setSshServerMode(v);
if (v === "native") {
form.setValue(
"authDaemonMode",
"native"
);
form.setValue(
"authDaemonPort",
null
);
} else {
form.setValue(
"authDaemonMode",
"site"
);
}
}}
cols={2}
/>
{authDaemonMode === "remote" && (
</div>
{/* Auth Method (standard only) */}
{!isNative && (
<div className="space-y-3">
<p className="text-sm font-semibold">
{t("sshAuthenticationMethod")}
</p>
<FormField
control={form.control}
name="authDaemonPort"
name="pamMode"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"internalResourceAuthDaemonPort"
)}
</FormLabel>
<FormControl>
<Input
type="number"
min={1}
max={65535}
placeholder="22123"
{...field}
disabled={
sshSectionDisabled
}
<StrategySelect<
"passthrough" | "push"
>
value={
field.value ?? ""
field.value ??
"passthrough"
}
onChange={(e) => {
options={[
{
id: "passthrough",
title: t(
"sshAuthMethodManual"
),
description: t(
"sshAuthMethodManualDescription"
),
disabled:
sshSectionDisabled
},
{
id: "push",
title: t(
"sshAuthMethodAutomated"
),
description: t(
"sshAuthMethodAutomatedDescription"
),
disabled:
sshSectionDisabled
}
]}
onChange={(v) => {
if (
sshSectionDisabled
)
return;
const v =
e.target.value;
if (v === "") {
field.onChange(
field.onChange(v);
if (
v ===
"passthrough"
) {
form.setValue(
"authDaemonPort",
null
);
return;
}
const num =
parseInt(v, 10);
field.onChange(
Number.isNaN(
num
)
? null
: num
);
}}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</div>
)}
{/* Daemon Location (standard + push) */}
{showDaemonLocation && (
<div className="space-y-3">
<p className="text-sm font-semibold">
{t("sshAuthDaemonLocation")}
</p>
<FormField
control={form.control}
name="authDaemonMode"
render={({ field }) => (
<FormItem>
<FormControl>
<StrategySelect<
"site" | "remote"
>
value={
(field.value as
| "site"
| "remote") ??
"site"
}
options={[
{
id: "site",
title: t(
"internalResourceAuthDaemonSite"
),
description: t(
"sshDaemonLocationSiteDescription"
),
disabled:
sshSectionDisabled
},
{
id: "remote",
title: t(
"sshDaemonLocationRemote"
),
description: t(
"sshDaemonLocationRemoteDescription"
),
disabled:
sshSectionDisabled
}
]}
onChange={(v) => {
if (
sshSectionDisabled
)
return;
field.onChange(v);
if (v === "site") {
form.setValue(
"authDaemonPort",
null
);
}
}}
cols={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<p className="text-sm text-muted-foreground">
{t("sshDaemonDisclaimer")}{" "}
<a
href="https://docs.pangolin.net/manage/resources/private/ssh"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-1"
>
{t("learnMore")}
<ExternalLink className="size-3.5 shrink-0" />
</a>
</p>
</div>
)}
{/* Daemon Port (standard + push + remote) */}
{showDaemonPort && (
<FormField
control={form.control}
name="authDaemonPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("sshDaemonPort")}
</FormLabel>
<FormControl>
<Input
type="number"
min={1}
max={65535}
placeholder="22123"
disabled={
sshSectionDisabled
}
value={field.value ?? ""}
onChange={(e) => {
if (sshSectionDisabled)
return;
const v =
e.target.value;
if (v === "") {
field.onChange(
null
);
return;
}
const num = parseInt(
v,
10
);
field.onChange(
Number.isNaN(num)
? null
: num
);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
)}
</HorizontalTabs>