Compare commits

...

7 Commits

Author SHA1 Message Date
Owen Schwartz e7fdbf9e85 Merge pull request #3699 from fosrl/dev
1.22.1-s.1
2026-09-04 10:38:39 -04:00
Owen 778a840ed7 Fix react bug not making it possible to complete security form 2026-09-04 10:08:57 -04:00
Owen 9d19195089 Configurable tab title and disable flag for keys 2026-09-04 09:20:20 -04:00
Owen 54bbe82504 Dont log invalid message type
Fixes #3695
2026-09-04 09:06:58 -04:00
Milo Schwartz cddb5ecc3d Merge pull request #3691 from fosrl/dev
update readme
2026-09-03 16:19:47 -04:00
miloschwartz de57df2520 update readme and screenshots 2026-09-03 16:17:07 -04:00
Owen 9e392a967d Add AI disclosure 2026-09-03 15:41:12 -04:00
20 changed files with 92 additions and 21 deletions
+8
View File
@@ -34,6 +34,14 @@ body:
validations: validations:
required: true required: true
- type: textarea
attributes:
label: AI Disclosure
description: |
If you used AI to help write this issue, please disclose it here. This is important for transparency and helps maintain the integrity of the issue tracking process.
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
+16 -5
View File
@@ -37,11 +37,22 @@
<p align="center"> <p align="center">
<strong> <strong>
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a> Get started with Pangolin Cloud at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
</strong> </strong>
</p> </p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control. Pangolin is an open-source SASE platform, built on WireGuard®, with a simple mission: connect and protect your users, wherever they are. It brings networking and security together as one system including a zero-trust VPN, zero-trust reverse proxy, privileged access management, and an identity-aware AI gateway, all sharing one identity and policy model. It's the same idea behind platforms like Cloudflare One, Zscaler, and Prisma but open, self-hostable, and built to stay light and easy to deploy.
### Networking and security that's unified, open, and simple
Legacy SASE platforms got the idea right: connectivity and security belong together. But they delivered it as a heavyweight, closed, cloud-locked stack assembled from years of patchwork. Pangolin exists to do that unification differently, in the open, self-hostable, and simple enough that administrators actually enjoy running it.
* **Open source, not a black box**: the code is open and auditable, so you can see exactly how your traffic is handled and how access decisions get made, instead of trusting a closed cloud control plane.
* **Networking and security as one platform**: sites, reverse proxy, client access, RBAC, and the AI gateway share one identity and policy model, so protecting users and connecting them are executed together.
* **Lightweight by design**: the whole platform is built to stay small and fast: easy to self-host on a small server, with a lightweight, user-space connector that goes in your private networks.
* **Enjoyable to use**: a clean, modern interface and a setup flow that gets out of your way, so managing access feels simple instead of like fighting a legacy admin console.
* **Zero trust from day one**: access is granted per resource, not per network, with identity provider integration, role-based access control, and full audit logging.
* **Run it your way**: self-host the Community Edition for free, step up to the Enterprise Edition for advanced features, or use Pangolin Cloud if you'd rather not manage infrastructure at all.
## Installation ## Installation
@@ -53,9 +64,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
## Deployment Options ## Deployment Options
- **Pangolin Cloud** - Fully managed service - no infrastructure required. - **Pangolin Cloud** - Fully managed service with no infrastructure required.
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3. - **Self-Host: Community Edition** - Free, open-source, and licensed under AGPL-3.
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue. - **Self-Host: Enterprise Edition** - Open-core, and licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
## Key Features ## Key Features
Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 800 KiB

+5
View File
@@ -112,6 +112,11 @@ export class Config {
? "true" ? "true"
: "false"; : "false";
process.env.FLAGS_DISABLE_VIRTUAL_API_KEYS_UI = parsedConfig.flags
?.disable_virtual_api_keys_ui
? "true"
: "false";
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
} }
+1
View File
@@ -442,6 +442,7 @@ export const configSchema = z
disable_config_managed_domains: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(),
disable_product_help_banners: z.boolean().optional(), disable_product_help_banners: z.boolean().optional(),
disable_enterprise_features: z.boolean().optional(), disable_enterprise_features: z.boolean().optional(),
disable_virtual_api_keys_ui: z.boolean().optional(),
enable_acme_cert_sync: z.boolean().optional().default(true), enable_acme_cert_sync: z.boolean().optional().default(true),
disable_private_http_placeholder: z disable_private_http_placeholder: z
.boolean() .boolean()
+2 -1
View File
@@ -104,7 +104,8 @@ const processMessage = async (
const handler = messageHandlers[message.type]; const handler = messageHandlers[message.type];
if (!handler) { if (!handler) {
throw new Error(`Unsupported message type: ${message.type}`); logger.debug(`No handler found for message type: ${message.type}`);
return;
} }
const response = await handler({ const response = await handler({
+4 -1
View File
@@ -353,7 +353,10 @@ const setupConnection = async (
const handler = messageHandlers[message.type]; const handler = messageHandlers[message.type];
if (!handler) { if (!handler) {
throw new Error(`Unsupported message type: ${message.type}`); logger.debug(
`No handler found for message type: ${message.type}`
);
return;
} }
const response = await handler({ const response = await handler({
+6 -1
View File
@@ -76,6 +76,11 @@ export default async function KeysPage(props: KeysPageProps) {
redirect("/"); redirect("/");
} }
const env = pullEnv();
if (env.flags.disableVirtualApiKeysUi) {
redirect(`/${orgId}`);
}
let keysData: ListMyVirtualApiKeysResponse | null = null; let keysData: ListMyVirtualApiKeysResponse | null = null;
try { try {
const res = await internal.get< const res = await internal.get<
@@ -90,7 +95,6 @@ export default async function KeysPage(props: KeysPageProps) {
redirect(`/${orgId}`); redirect(`/${orgId}`);
} }
const env = pullEnv();
const primaryOrg = orgs.find((o) => o.orgId === orgId)?.isPrimaryOrg; const primaryOrg = orgs.find((o) => o.orgId === orgId)?.isPrimaryOrg;
const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner); const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner);
@@ -106,6 +110,7 @@ export default async function KeysPage(props: KeysPageProps) {
showSidebar={false} showSidebar={false}
launcherMode launcherMode
showViewAsAdmin={isAdminOrOwner} showViewAsAdmin={isAdminOrOwner}
env={env}
> >
<UserVirtualApiKeys orgId={orgId} initialData={keysData} /> <UserVirtualApiKeys orgId={orgId} initialData={keysData} />
</Layout> </Layout>
+1
View File
@@ -83,6 +83,7 @@ export default async function OrgPage(props: OrgPageProps) {
showSidebar={false} showSidebar={false}
launcherMode launcherMode
showViewAsAdmin={isAdminOrOwner} showViewAsAdmin={isAdminOrOwner}
env={env}
> >
{overview && launcherData ? ( {overview && launcherData ? (
<ResourceLauncher <ResourceLauncher
@@ -117,6 +117,7 @@ export default function SecurityPage() {
} }
function LogRetentionSectionForm({ org }: SectionFormProps) { function LogRetentionSectionForm({ org }: SectionFormProps) {
const { updateOrg } = useOrgContext();
const form = useForm({ const form = useForm({
resolver: zodResolver( resolver: zodResolver(
SecurityFormSchema.pick({ SecurityFormSchema.pick({
@@ -173,6 +174,11 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Update organization // Update organization
await api.post(`/org/${org.orgId}`, reqData); await api.post(`/org/${org.orgId}`, reqData);
// Update the org context immediately so the dropdowns reflect
// the saved values without waiting on a re-fetch that could
// race a lagging read replica
updateOrg(reqData);
toast({ toast({
title: t("orgUpdated"), title: t("orgUpdated"),
description: t("orgUpdatedDescription") description: t("orgUpdatedDescription")
@@ -199,7 +205,10 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
formAction();
}}
className="grid gap-4" className="grid gap-4"
id="org-log-retention-settings-form" id="org-log-retention-settings-form"
> >
@@ -827,6 +836,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
function SecuritySettingsSectionForm({ org }: SectionFormProps) { function SecuritySettingsSectionForm({ org }: SectionFormProps) {
const router = useRouter(); const router = useRouter();
const { updateOrg } = useOrgContext();
const form = useForm({ const form = useForm({
resolver: zodResolver( resolver: zodResolver(
SecurityFormSchema.pick({ SecurityFormSchema.pick({
@@ -899,6 +909,11 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
// Update organization // Update organization
await api.post(`/org/${org.orgId}`, reqData); await api.post(`/org/${org.orgId}`, reqData);
// Update the org context immediately so the dropdowns reflect
// the saved values without waiting on a re-fetch that could
// race a lagging read replica
updateOrg(reqData);
toast({ toast({
title: t("orgUpdated"), title: t("orgUpdated"),
description: t("orgUpdatedDescription") description: t("orgUpdatedDescription")
@@ -942,7 +957,10 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
formAction();
}}
ref={formRef} ref={formRef}
id="security-settings-section-form" id="security-settings-section-form"
className="space-y-4" className="space-y-4"
@@ -30,9 +30,15 @@ import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: "Resource Access" const env = pullEnv();
}; const title =
env.branding.resourceAuthPage?.titleText ||
env.branding.appName ||
"Resource Access";
return { title };
}
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
+5 -1
View File
@@ -53,17 +53,21 @@ export type OrgNavSectionsOptions = {
}; };
// Merged from 'user-management-and-resources' branch // Merged from 'user-management-and-resources' branch
export const orgLangingNavItems: SidebarNavItem[] = [ export const orgLangingNavItems = (env?: Env): SidebarNavItem[] => [
{ {
title: "sidebarAccount", title: "sidebarAccount",
href: "/{orgId}", href: "/{orgId}",
icon: <LayoutGrid className="size-4 flex-none" /> icon: <LayoutGrid className="size-4 flex-none" />
}, },
...(!env?.flags.disableVirtualApiKeysUi
? [
{ {
title: "sidebarMyApiKeys", title: "sidebarMyApiKeys",
href: "/{orgId}/keys", href: "/{orgId}/keys",
icon: <KeyRound className="size-4 flex-none" /> icon: <KeyRound className="size-4 flex-none" />
} }
]
: [])
]; ];
export const orgNavSections = ( export const orgNavSections = (
+5 -2
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org"; import { ListUserOrgsResponse } from "@server/routers/org";
import { Env } from "@app/lib/types/env";
import { import {
orgLangingNavItems, orgLangingNavItems,
type CommandBarNavSection, type CommandBarNavSection,
@@ -25,6 +26,7 @@ interface LayoutProps {
defaultSidebarCollapsed?: boolean; defaultSidebarCollapsed?: boolean;
launcherMode?: boolean; launcherMode?: boolean;
showViewAsAdmin?: boolean; showViewAsAdmin?: boolean;
env?: Env;
} }
export async function Layout({ export async function Layout({
@@ -38,7 +40,8 @@ export async function Layout({
showTopBar = true, showTopBar = true,
defaultSidebarCollapsed = false, defaultSidebarCollapsed = false,
launcherMode = false, launcherMode = false,
showViewAsAdmin = false showViewAsAdmin = false,
env
}: LayoutProps) { }: LayoutProps) {
const allCookies = await cookies(); const allCookies = await cookies();
const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value; const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value;
@@ -49,7 +52,7 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed); (sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
const launcherNavItems: SidebarNavItem[] = launcherMode const launcherNavItems: SidebarNavItem[] = launcherMode
? orgLangingNavItems ? orgLangingNavItems(env)
: []; : [];
return ( return (
+4
View File
@@ -70,6 +70,10 @@ export function pullEnv(): Env {
: false, : false,
disableEnterpriseFeatures: disableEnterpriseFeatures:
process.env.DISABLE_ENTERPRISE_FEATURES === "true" process.env.DISABLE_ENTERPRISE_FEATURES === "true"
? true
: false,
disableVirtualApiKeysUi:
process.env.FLAGS_DISABLE_VIRTUAL_API_KEYS_UI === "true"
? true ? true
: false : false
}, },
+1
View File
@@ -36,6 +36,7 @@ export type Env = {
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
disableEnterpriseFeatures: boolean; disableEnterpriseFeatures: boolean;
disableVirtualApiKeysUi: boolean;
}; };
branding: { branding: {
appName?: string; appName?: string;