From 8addee3bdd5c04bb9b8ad36a2f97935ca13f0642 Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Tue, 16 Dec 2025 14:25:43 +0000 Subject: [PATCH] feat: enhance network policy with INTERNET_ONLY options and update related components --- prisma/schema.prisma | 4 +- .../app/[appId]/advanced/basic-auth.tsx | 2 +- .../app/[appId]/advanced/network-policy.tsx | 2 + src/app/project/app/[appId]/layout.tsx | 22 ++-- src/server/services/network-policy.service.ts | 105 +++++++++++++++--- .../mariadb-backup.service.ts | 1 + .../mongodb-backup.service.ts | 1 + .../postgres-backup.service.ts | 1 + src/shared/model/network-policy.model.ts | 2 +- src/shared/utils/constants.ts | 2 + 10 files changed, 113 insertions(+), 29 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ca3964f..5320fc8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -200,8 +200,8 @@ model App { webhookId String? - ingressNetworkPolicy String @default("ALLOW_ALL") // ALLOW_ALL, NAMESPACE_ONLY, DENY_ALL - egressNetworkPolicy String @default("ALLOW_ALL") // ALLOW_ALL, NAMESPACE_ONLY, DENY_ALL + ingressNetworkPolicy String @default("ALLOW_ALL") // ALLOW_ALL, NAMESPACE_ONLY, DENY_ALL, INTERNET_ONLY + egressNetworkPolicy String @default("ALLOW_ALL") // ALLOW_ALL, NAMESPACE_ONLY, DENY_ALL, INTERNET_ONLY appDomains AppDomain[] appPorts AppPort[] diff --git a/src/app/project/app/[appId]/advanced/basic-auth.tsx b/src/app/project/app/[appId]/advanced/basic-auth.tsx index ad909b1..a2a094c 100644 --- a/src/app/project/app/[appId]/advanced/basic-auth.tsx +++ b/src/app/project/app/[appId]/advanced/basic-auth.tsx @@ -39,7 +39,7 @@ export default function BasicAuth({ app, readonly }: { - {app.appFileMounts.length} Auth Credentials + {app.appBasicAuths.length} Auth Credentials Username diff --git a/src/app/project/app/[appId]/advanced/network-policy.tsx b/src/app/project/app/[appId]/advanced/network-policy.tsx index 8913227..163cfb8 100644 --- a/src/app/project/app/[appId]/advanced/network-policy.tsx +++ b/src/app/project/app/[appId]/advanced/network-policy.tsx @@ -44,6 +44,7 @@ export default function NetworkPolicy({ app, readonly }: { Allow All (Internet + Project Apps) + Internet Only Project Apps Only Deny All @@ -64,6 +65,7 @@ export default function NetworkPolicy({ app, readonly }: { Allow All (Internet + Project Apps) + Internet Only Project Apps Only Deny All diff --git a/src/app/project/app/[appId]/layout.tsx b/src/app/project/app/[appId]/layout.tsx index b173e50..1c9b675 100644 --- a/src/app/project/app/[appId]/layout.tsx +++ b/src/app/project/app/[appId]/layout.tsx @@ -20,7 +20,7 @@ export default async function RootLayout({ const session = await isAuthorizedReadForApp(appId); const app = await appService.getExtendedById(appId); - const showIngressWarning = app.appDomains.length > 0 && app.ingressNetworkPolicy !== 'ALLOW_ALL'; + const showIngressWarning = app.appDomains.length > 0 && app.ingressNetworkPolicy !== 'ALLOW_ALL' && app.ingressNetworkPolicy !== 'INTERNET_ONLY'; return (
@@ -28,16 +28,16 @@ export default async function RootLayout({ title={app.name} subtitle={`App ID: ${app.id}`}> - {showIngressWarning && ( - - - Warning - - You have configured domains for this app, but the Ingress Network Policy is not set to "Allow All". - External traffic via the domain might be blocked. - - - )} + {showIngressWarning && ( + + + Warning + + You have configured domains for this app, but the Ingress Network Policy is not set to "Allow All" or "Internet Only". + External traffic via the domain might be blocked. + + + )} {children}
diff --git a/src/server/services/network-policy.service.ts b/src/server/services/network-policy.service.ts index 69c9a7e..6b0b1e4 100644 --- a/src/server/services/network-policy.service.ts +++ b/src/server/services/network-policy.service.ts @@ -45,11 +45,73 @@ class NetworkPolicyService { if (policyType === 'ALLOW_ALL') { // Allow from everywhere rules.push({ - from: [{ - ipBlock: { - cidr: '0.0.0.0/0' + from: [ + { + ipBlock: { + cidr: '0.0.0.0/0', + except: [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.0/8' + ] + } + }, + { + // Allow from Traefik ingress controller + namespaceSelector: { + matchLabels: { + 'kubernetes.io/metadata.name': 'kube-system' + } + }, + podSelector: { + matchLabels: { + 'app.kubernetes.io/name': 'traefik' + } + } + }, + { + podSelector: {} // Selects all pods in the same namespace } - }] + ] + }); + } else if (policyType === 'INTERNET_ONLY') { + // Allow from internet (external to cluster) and from Traefik ingress controller + // Block other internal pod traffic + rules.push({ + from: [ + { + ipBlock: { + cidr: '0.0.0.0/0', + except: [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.0/8' + ] + } + }, + { + // Allow from Traefik ingress controller + namespaceSelector: { + matchLabels: { + 'kubernetes.io/metadata.name': 'kube-system' + } + }, + podSelector: { + matchLabels: { + 'app.kubernetes.io/name': 'traefik' + } + } + }, + { + podSelector: { + matchLabels: { + [Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB + } + } + } + ] }); } else if (policyType === 'NAMESPACE_ONLY') { // Allow only from same namespace @@ -59,7 +121,16 @@ class NetworkPolicyService { }] }); } else if (policyType === 'DENY_ALL') { - // No rules means deny all + // No rules means deny all --> except the separate container for database backups + rules.push({ + from: [{ + podSelector: { + matchLabels: { + [Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB + } + } + }] + }); } return rules; @@ -84,16 +155,7 @@ class NetworkPolicyService { "k8s-app": "kube-dns" } } - }, - { - ipBlock: { - cidr: '0.0.0.0/0' - } } - ], - ports: [ - { protocol: 'UDP', port: 53 as any }, - { protocol: 'TCP', port: 53 as any } ] }); @@ -116,6 +178,21 @@ class NetworkPolicyService { } ] }); + } else if (policyType === 'INTERNET_ONLY') { + // Allow only to internet, block internal cluster traffic + rules.push({ + to: [{ + ipBlock: { + cidr: '0.0.0.0/0', + except: [ + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.0/8' + ] + } + }] + }); } else if (policyType === 'NAMESPACE_ONLY') { // Allow only to same namespace rules.push({ diff --git a/src/server/services/standalone-services/database-backup-services/mariadb-backup.service.ts b/src/server/services/standalone-services/database-backup-services/mariadb-backup.service.ts index d840585..0eee7fc 100644 --- a/src/server/services/standalone-services/database-backup-services/mariadb-backup.service.ts +++ b/src/server/services/standalone-services/database-backup-services/mariadb-backup.service.ts @@ -42,6 +42,7 @@ class MariaDbBackupService { annotations: { [Constants.QS_ANNOTATION_APP_ID]: app.id, [Constants.QS_ANNOTATION_PROJECT_ID]: app.projectId, + [Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB } }, spec: { diff --git a/src/server/services/standalone-services/database-backup-services/mongodb-backup.service.ts b/src/server/services/standalone-services/database-backup-services/mongodb-backup.service.ts index b9f5588..84884d0 100644 --- a/src/server/services/standalone-services/database-backup-services/mongodb-backup.service.ts +++ b/src/server/services/standalone-services/database-backup-services/mongodb-backup.service.ts @@ -43,6 +43,7 @@ class MongoDbBackupService { annotations: { [Constants.QS_ANNOTATION_APP_ID]: app.id, [Constants.QS_ANNOTATION_PROJECT_ID]: app.projectId, + [Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB } }, spec: { diff --git a/src/server/services/standalone-services/database-backup-services/postgres-backup.service.ts b/src/server/services/standalone-services/database-backup-services/postgres-backup.service.ts index 99de043..2c1c424 100644 --- a/src/server/services/standalone-services/database-backup-services/postgres-backup.service.ts +++ b/src/server/services/standalone-services/database-backup-services/postgres-backup.service.ts @@ -42,6 +42,7 @@ class PostgresBackupService { annotations: { [Constants.QS_ANNOTATION_APP_ID]: app.id, [Constants.QS_ANNOTATION_PROJECT_ID]: app.projectId, + [Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB } }, spec: { diff --git a/src/shared/model/network-policy.model.ts b/src/shared/model/network-policy.model.ts index e2a56c5..be73d54 100644 --- a/src/shared/model/network-policy.model.ts +++ b/src/shared/model/network-policy.model.ts @@ -1,4 +1,4 @@ import { z } from "zod"; -export const appNetworkPolicy = z.enum(["ALLOW_ALL", "NAMESPACE_ONLY", "DENY_ALL"]); +export const appNetworkPolicy = z.enum(["ALLOW_ALL", "INTERNET_ONLY", "NAMESPACE_ONLY", "DENY_ALL"]); export type AppNetworkPolicyType = z.infer; \ No newline at end of file diff --git a/src/shared/utils/constants.ts b/src/shared/utils/constants.ts index 57ad686..3c8ccec 100644 --- a/src/shared/utils/constants.ts +++ b/src/shared/utils/constants.ts @@ -1,6 +1,8 @@ export class Constants { static readonly QS_ANNOTATION_APP_ID = 'qs-app-id'; static readonly QS_ANNOTATION_PROJECT_ID = 'qs-project-id'; + static readonly QS_ANNOTATION_CONTAINER_TYPE = 'qs-containter-type'; + static readonly QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB = 'qs-job-database-backup'; static readonly QS_ANNOTATION_DEPLOYMENT_ID = 'qs-deplyoment-id'; static readonly QS_ANNOTATION_GIT_COMMIT = 'qs-git-commit'; static readonly K3S_JOIN_TOKEN = 'k3sJoinToken';