mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-16 04:57:52 +00:00
Add flag for generate own certs
This commit is contained in:
@@ -8,9 +8,7 @@ import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||
import {
|
||||
getValidCertificatesForDomains,
|
||||
} from "#dynamic/lib/certificates";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
import { build } from "@server/build";
|
||||
|
||||
@@ -311,6 +309,10 @@ export class TraefikConfigManager {
|
||||
this.lastActiveDomains = new Set(domains);
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.GENERATE_OWN_CERTIFICATES === "true" &&
|
||||
build != "oss"
|
||||
) {
|
||||
// Scan current local certificate state
|
||||
this.lastLocalCertificateState =
|
||||
await this.scanLocalCertificateState();
|
||||
@@ -347,7 +349,9 @@ export class TraefikConfigManager {
|
||||
if (domainsToFetch.size > 0) {
|
||||
// Get valid certificates for domains not covered by wildcards
|
||||
validCertificates =
|
||||
await getValidCertificatesForDomains(domainsToFetch);
|
||||
await getValidCertificatesForDomains(
|
||||
domainsToFetch
|
||||
);
|
||||
this.lastCertificateFetch = new Date();
|
||||
this.lastKnownDomains = new Set(domains);
|
||||
|
||||
@@ -370,7 +374,8 @@ export class TraefikConfigManager {
|
||||
} else {
|
||||
const timeSinceLastFetch = this.lastCertificateFetch
|
||||
? Math.round(
|
||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
||||
(Date.now() -
|
||||
this.lastCertificateFetch.getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: 0;
|
||||
@@ -388,6 +393,7 @@ export class TraefikConfigManager {
|
||||
|
||||
// wait 1 second for traefik to pick up the new certificates
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
// Write traefik config as YAML to a second dynamic config file if changed
|
||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||
@@ -690,7 +696,12 @@ export class TraefikConfigManager {
|
||||
|
||||
for (const cert of validCertificates) {
|
||||
try {
|
||||
if (!cert.certFile || !cert.keyFile) {
|
||||
if (
|
||||
!cert.certFile ||
|
||||
!cert.keyFile ||
|
||||
cert.certFile.length === 0 ||
|
||||
cert.keyFile.length === 0
|
||||
) {
|
||||
logger.warn(
|
||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||
);
|
||||
|
||||
@@ -105,7 +105,12 @@ export async function getTraefikConfig(
|
||||
const priority = row.priority ?? 100;
|
||||
|
||||
// Create a unique key combining resourceId, path config, and rewrite config
|
||||
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
|
||||
const pathKey = [
|
||||
targetPath,
|
||||
pathMatchType,
|
||||
rewritePath,
|
||||
rewritePathType
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-");
|
||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||
@@ -120,7 +125,9 @@ export async function getTraefikConfig(
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
|
||||
logger.error(
|
||||
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,9 +246,7 @@ export async function getTraefikConfig(
|
||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||
}
|
||||
|
||||
let tls = {};
|
||||
if (build == "oss") {
|
||||
tls = {
|
||||
const tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
? {
|
||||
@@ -253,7 +258,6 @@ export async function getTraefikConfig(
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
@@ -264,11 +268,12 @@ export async function getTraefikConfig(
|
||||
];
|
||||
|
||||
// Handle path rewriting middleware
|
||||
if (resource.rewritePath &&
|
||||
if (
|
||||
resource.rewritePath &&
|
||||
resource.path &&
|
||||
resource.pathMatchType &&
|
||||
resource.rewritePathType) {
|
||||
|
||||
resource.rewritePathType
|
||||
) {
|
||||
// Create a unique middleware name
|
||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
||||
|
||||
@@ -287,7 +292,10 @@ export async function getTraefikConfig(
|
||||
}
|
||||
|
||||
// the middleware to the config
|
||||
Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
|
||||
Object.assign(
|
||||
config_output.http.middlewares,
|
||||
rewriteResult.middlewares
|
||||
);
|
||||
|
||||
// middlewares to the router middleware chain
|
||||
if (rewriteResult.chain) {
|
||||
@@ -298,9 +306,13 @@ export async function getTraefikConfig(
|
||||
routerMiddlewares.push(rewriteMiddlewareName);
|
||||
}
|
||||
|
||||
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
|
||||
logger.debug(
|
||||
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
|
||||
logger.error(
|
||||
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +328,9 @@ export async function getTraefikConfig(
|
||||
value: string;
|
||||
}[];
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
|
||||
logger.warn(
|
||||
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
headersArr.forEach((header) => {
|
||||
|
||||
@@ -148,6 +148,10 @@ export class PrivateConfig {
|
||||
if (parsedPrivateConfig.stripe?.s3Region) {
|
||||
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
||||
}
|
||||
if (parsedPrivateConfig.flags?.generate_own_certificates) {
|
||||
process.env.GENERATE_OWN_CERTIFICATES =
|
||||
parsedPrivateConfig.flags.generate_own_certificates.toString();
|
||||
}
|
||||
}
|
||||
|
||||
this.rawPrivateConfig = parsedPrivateConfig;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { MemoryStore, Store } from "express-rate-limit";
|
||||
import RedisStore from "#private/lib/redisStore";
|
||||
|
||||
export function createStore(): Store {
|
||||
if (build != "oss" && privateConfig.getRawPrivateConfig().flags?.enable_redis) {
|
||||
if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
|
||||
const rateLimitStore: Store = new RedisStore({
|
||||
prefix: "api-rate-limit", // Optional: customize Redis key prefix
|
||||
skipFailedRequests: true, // Don't count failed requests
|
||||
|
||||
@@ -20,23 +20,28 @@ import { build } from "@server/build";
|
||||
|
||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||
|
||||
export const privateConfigSchema = z
|
||||
export const privateConfigSchema = z.object({
|
||||
app: z
|
||||
.object({
|
||||
app: z.object({
|
||||
region: z.string().optional().default("default"),
|
||||
base_domain: z.string().optional()
|
||||
}).optional().default({
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
region: "default"
|
||||
}),
|
||||
server: z.object({
|
||||
server: z
|
||||
.object({
|
||||
encryption_key_path: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("./config/encryption.pem")
|
||||
.pipe(z.string().min(8)),
|
||||
resend_api_key: z.string().optional(),
|
||||
reo_client_id: z.string().optional(),
|
||||
}).optional().default({
|
||||
reo_client_id: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
encryption_key_path: "./config/encryption.pem"
|
||||
}),
|
||||
redis: z
|
||||
@@ -67,15 +72,20 @@ export const privateConfigSchema = z
|
||||
.optional(),
|
||||
gerbil: z
|
||||
.object({
|
||||
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
|
||||
local_exit_node_reachable_at: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("http://gerbil:3003")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
flags: z
|
||||
.object({
|
||||
enable_redis: z.boolean().optional(),
|
||||
enable_redis: z.boolean().optional().default(false),
|
||||
generate_own_certificates: z.boolean().optional().default(false)
|
||||
})
|
||||
.optional(),
|
||||
.optional()
|
||||
.default({}),
|
||||
branding: z
|
||||
.object({
|
||||
app_name: z.string().optional(),
|
||||
@@ -153,8 +163,8 @@ export const privateConfigSchema = z
|
||||
s3Region: z.string().default("us-east-1"),
|
||||
localFilePath: z.string()
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
.optional()
|
||||
});
|
||||
|
||||
export function readPrivateConfigFile() {
|
||||
if (build == "oss") {
|
||||
@@ -182,9 +192,7 @@ export function readPrivateConfigFile() {
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
"No private configuration file found."
|
||||
);
|
||||
throw new Error("No private configuration file found.");
|
||||
}
|
||||
|
||||
return environment;
|
||||
|
||||
@@ -46,7 +46,7 @@ class RedisManager {
|
||||
this.isEnabled = false;
|
||||
return;
|
||||
}
|
||||
this.isEnabled = privateConfig.getRawPrivateConfig().flags?.enable_redis || false;
|
||||
this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
|
||||
if (this.isEnabled) {
|
||||
this.initializeClients();
|
||||
}
|
||||
|
||||
@@ -21,11 +21,10 @@ import {
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import config from "@server/lib/config";
|
||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||
import { build } from "@server/build";
|
||||
import { sanitize } from "@server/lib/traefik/utils";
|
||||
import privateConfig from "#private/lib/config";
|
||||
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
const redirectToRootMiddlewareName = "redirect-to-root";
|
||||
@@ -234,12 +233,13 @@ export async function getTraefikConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resource.certificateStatus !== "valid") {
|
||||
logger.debug(
|
||||
`Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
|
||||
// if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||
// logger.debug(
|
||||
// `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
||||
// );
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// add routers and services empty objects if they don't exist
|
||||
if (!config_output.http.routers) {
|
||||
@@ -264,6 +264,11 @@ export async function getTraefikConfig(
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let tls = {};
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags
|
||||
.generate_own_certificates
|
||||
) {
|
||||
let certResolver: string, preferWildcardCert: boolean;
|
||||
if (!configDomain) {
|
||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||
@@ -274,8 +279,6 @@ export async function getTraefikConfig(
|
||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||
}
|
||||
|
||||
let tls = {};
|
||||
if (build == "oss") {
|
||||
tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
|
||||
@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { Transaction } from "@server/db";
|
||||
import { eq, or, and, like } from "drizzle-orm";
|
||||
import { build } from "@server/build";
|
||||
import privateConfig from "#private/lib/config";
|
||||
|
||||
/**
|
||||
* Checks if a certificate exists for the given domain.
|
||||
* If not, creates a new certificate in 'pending' state.
|
||||
* Wildcard certs cover subdomains.
|
||||
*/
|
||||
export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) {
|
||||
if (build !== "saas") {
|
||||
export async function createCertificate(
|
||||
domainId: string,
|
||||
domain: string,
|
||||
trx: Transaction | typeof db
|
||||
) {
|
||||
if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
||||
|
||||
let existing: Certificate[] = [];
|
||||
if (domainRecord.type == "ns") {
|
||||
const domainLevelDown = domain.split('.').slice(1).join('.');
|
||||
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||
existing = await trx
|
||||
.select()
|
||||
.from(certificates)
|
||||
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
||||
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
||||
or(
|
||||
eq(certificates.domain, domain),
|
||||
eq(certificates.domain, domainLevelDown),
|
||||
eq(certificates.domain, domainLevelDown)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
logger.info(
|
||||
`Certificate already exists for domain ${domain}`
|
||||
);
|
||||
logger.info(`Certificate already exists for domain ${domain}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,8 @@ export default function ResourceRules(props: {
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const env = useEnvContext();
|
||||
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0;
|
||||
const { env } = useEnvContext();
|
||||
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||
|
||||
const RuleAction = {
|
||||
ACCEPT: t('alwaysAllow'),
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||
import { toUnicode } from 'punycode';
|
||||
import { toUnicode } from "punycode";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type ResourceInfoBoxType = {};
|
||||
|
||||
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
||||
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
const { resource, authInfo } = useResourceContext();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -28,7 +30,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{/* 4 cols because of the certs */}
|
||||
<InfoSections cols={resource.http && build != "oss" ? 4 : 3}>
|
||||
<InfoSections
|
||||
cols={
|
||||
resource.http && env.flags.generateOwnCertificates
|
||||
? 4
|
||||
: 3
|
||||
}
|
||||
>
|
||||
{resource.http ? (
|
||||
<>
|
||||
<InfoSection>
|
||||
@@ -126,7 +134,10 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
||||
{/* </InfoSectionContent> */}
|
||||
{/* </InfoSection> */}
|
||||
{/* Certificate Status Column */}
|
||||
{resource.http && resource.domainId && resource.fullDomain && build != "oss" && (
|
||||
{resource.http &&
|
||||
resource.domainId &&
|
||||
resource.fullDomain &&
|
||||
build != "oss" && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("certificateStatus", {
|
||||
|
||||
@@ -80,6 +80,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
||||
const api = createApiClient(useEnvContext());
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const subscribed = subscription?.getTier() === TierId.STANDARD;
|
||||
@@ -435,8 +436,8 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
||||
</div>
|
||||
|
||||
{/* Certificate Status */}
|
||||
{(build !== "saas" ||
|
||||
(build === "saas" && subscribed)) &&
|
||||
{(
|
||||
(env.flags.generateOwnCertificates && subscribed)) &&
|
||||
loginPage?.domainId &&
|
||||
loginPage?.fullDomain &&
|
||||
!hasUnsavedChanges && (
|
||||
|
||||
@@ -48,7 +48,11 @@ export function pullEnv(): Env {
|
||||
enableClients:
|
||||
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
|
||||
hideSupporterKey:
|
||||
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false
|
||||
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
|
||||
generateOwnCertificates:
|
||||
process.env.GENERATE_OWN_CERTIFICATES === "true"
|
||||
? true
|
||||
: false
|
||||
},
|
||||
|
||||
branding: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export type Env = {
|
||||
disableBasicWireguardSites: boolean;
|
||||
enableClients: boolean;
|
||||
hideSupporterKey: boolean;
|
||||
generateOwnCertificates: boolean;
|
||||
},
|
||||
branding: {
|
||||
appName?: string;
|
||||
|
||||
Reference in New Issue
Block a user