mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 13:39:07 -06:00
feat: add help dialog for network policy types in UI
This commit is contained in:
@@ -8,6 +8,8 @@ import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { saveNetworkPolicy } from "./actions";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
|
||||
export default function NetworkPolicy({ app, readonly }: {
|
||||
app: AppExtendedModel;
|
||||
@@ -15,6 +17,7 @@ export default function NetworkPolicy({ app, readonly }: {
|
||||
}) {
|
||||
const [ingressPolicy, setIngressPolicy] = useState(app.ingressNetworkPolicy);
|
||||
const [egressPolicy, setEgressPolicy] = useState(app.egressNetworkPolicy);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
await Toast.fromAction(() => saveNetworkPolicy(app.id, ingressPolicy, egressPolicy));
|
||||
@@ -77,8 +80,56 @@ export default function NetworkPolicy({ app, readonly }: {
|
||||
</div>
|
||||
</CardContent>
|
||||
{!readonly && (
|
||||
<CardFooter>
|
||||
<CardFooter className="gap-3">
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
<Dialog open={showHelp} onOpenChange={setShowHelp}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Network Policy Types</DialogTitle>
|
||||
<DialogDescription>
|
||||
Understand how each policy type controls traffic to and from your application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm">Allow All (Internet + Project Apps)</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Allows traffic from/to all apps within the same project and the internet.
|
||||
External internet traffic reaches your app through the Traefik ingress controller.
|
||||
Blocks traffic from/to other projects/namespaces.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm">Internet Only</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Allows traffic only from/to the internet (via Traefik ingress controller).
|
||||
Blocks all direct pod-to-pod communication within the cluster, including same-project apps.
|
||||
Useful for public-facing applications that should not communicate with internal services.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm">Project Apps Only</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Allows traffic only from/to apps within the same project.
|
||||
Blocks all internet traffic and traffic from other projects.
|
||||
Ideal for internal microservices that should only communicate within your project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-sm">Deny All</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Blocks all incoming or outgoing traffic.
|
||||
Use this for maximum isolation when your application should not communicate with any other service.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import k3s from "../adapter/kubernetes-api.adapter";
|
||||
import { V1NetworkPolicy, V1NetworkPolicyEgressRule, V1NetworkPolicyIngressRule } from "@kubernetes/client-node";
|
||||
import { V1NetworkPolicy, V1NetworkPolicyEgressRule, V1NetworkPolicyIngressRule, V1NetworkPolicyPeer } from "@kubernetes/client-node";
|
||||
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
|
||||
import { Constants } from "../../shared/utils/constants";
|
||||
import { appNetworkPolicy, AppNetworkPolicyType } from "@/shared/model/network-policy.model";
|
||||
|
||||
class NetworkPolicyService {
|
||||
|
||||
@@ -10,6 +11,9 @@ class NetworkPolicyService {
|
||||
const policyName = KubeObjectNameUtils.toNetworkPolicyName(app.id);
|
||||
const namespace = app.projectId;
|
||||
|
||||
const ingressPolicy = this.normalizePolicy(app.ingressNetworkPolicy);
|
||||
const egressPolicy = this.normalizePolicy(app.egressNetworkPolicy);
|
||||
|
||||
const policy: V1NetworkPolicy = {
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "NetworkPolicy",
|
||||
@@ -31,86 +35,75 @@ class NetworkPolicyService {
|
||||
}
|
||||
},
|
||||
policyTypes: ["Ingress", "Egress"],
|
||||
ingress: this.getIngressRules(app.ingressNetworkPolicy),
|
||||
egress: this.getEgressRules(app.egressNetworkPolicy)
|
||||
ingress: this.getIngressRules(ingressPolicy),
|
||||
egress: this.getEgressRules(egressPolicy)
|
||||
}
|
||||
};
|
||||
console.log(JSON.stringify(policy, null, 2));
|
||||
await this.applyNetworkPolicy(namespace, policyName, policy);
|
||||
}
|
||||
|
||||
private getIngressRules(policyType: string): V1NetworkPolicyIngressRule[] {
|
||||
private normalizePolicy(raw: string): AppNetworkPolicyType {
|
||||
const parsed = appNetworkPolicy.safeParse(raw);
|
||||
return parsed.success ? parsed.data : 'ALLOW_ALL';
|
||||
}
|
||||
|
||||
private getIngressRules(policyType: AppNetworkPolicyType): V1NetworkPolicyIngressRule[] {
|
||||
const rules: V1NetworkPolicyIngressRule[] = [];
|
||||
|
||||
const traefikFrom: V1NetworkPolicyPeer[] = [
|
||||
{
|
||||
namespaceSelector: {
|
||||
matchLabels: {
|
||||
'kubernetes.io/metadata.name': 'kube-system'
|
||||
}
|
||||
},
|
||||
podSelector: {
|
||||
matchLabels: {
|
||||
'app.kubernetes.io/name': 'traefik'
|
||||
}
|
||||
}
|
||||
},
|
||||
/* // Fallback label used in some clusters/charts
|
||||
{
|
||||
namespaceSelector: {
|
||||
matchLabels: {
|
||||
'kubernetes.io/metadata.name': 'kube-system'
|
||||
}
|
||||
},
|
||||
podSelector: {
|
||||
matchLabels: {
|
||||
app: 'traefik'
|
||||
}
|
||||
}
|
||||
}*/
|
||||
];
|
||||
|
||||
const backupPodFrom: V1NetworkPolicyPeer[] = [{
|
||||
podSelector: {
|
||||
matchLabels: {
|
||||
[Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (policyType === 'ALLOW_ALL') {
|
||||
// Allow from everywhere
|
||||
// Allow from same namespace and from Traefik (internet traffic comes through Traefik)
|
||||
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'
|
||||
}
|
||||
}
|
||||
},
|
||||
...traefikFrom,
|
||||
{
|
||||
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
|
||||
// Allow from Traefik (internet traffic comes through Traefik) and from DB-backup jobs.
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
...traefikFrom,
|
||||
...backupPodFrom
|
||||
]
|
||||
});
|
||||
} else if (policyType === 'NAMESPACE_ONLY') {
|
||||
@@ -123,26 +116,20 @@ class NetworkPolicyService {
|
||||
} else if (policyType === '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
|
||||
}
|
||||
}
|
||||
}]
|
||||
from: [
|
||||
...backupPodFrom
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
private getEgressRules(policyType: string): V1NetworkPolicyEgressRule[] {
|
||||
private getEgressRules(policyType: AppNetworkPolicyType): V1NetworkPolicyEgressRule[] {
|
||||
const rules: V1NetworkPolicyEgressRule[] = [];
|
||||
|
||||
// Always allow DNS
|
||||
// We allow UDP/TCP 53 to everywhere because kube-dns IP might vary or be outside the namespace
|
||||
// and we want to be safe.
|
||||
rules.push({
|
||||
// allow DNS (kube-dns/coredns) on UDP/TCP 53
|
||||
const dnsRuleAllow: V1NetworkPolicyEgressRule = {
|
||||
to: [
|
||||
{
|
||||
namespaceSelector: {
|
||||
@@ -155,12 +142,29 @@ class NetworkPolicyService {
|
||||
"k8s-app": "kube-dns"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
namespaceSelector: {
|
||||
matchLabels: {
|
||||
"kubernetes.io/metadata.name": "kube-system"
|
||||
}
|
||||
},
|
||||
podSelector: {
|
||||
matchLabels: {
|
||||
"k8s-app": "coredns"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
ports: [
|
||||
{ protocol: 'UDP', port: 53 as any },
|
||||
{ protocol: 'TCP', port: 53 as any }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
if (policyType === 'ALLOW_ALL') {
|
||||
// Allow Internet + Local Namespace, Block other namespaces (Private IPs)
|
||||
rules.push(dnsRuleAllow);
|
||||
rules.push({
|
||||
to: [
|
||||
{
|
||||
@@ -180,6 +184,7 @@ class NetworkPolicyService {
|
||||
});
|
||||
} else if (policyType === 'INTERNET_ONLY') {
|
||||
// Allow only to internet, block internal cluster traffic
|
||||
rules.push(dnsRuleAllow);
|
||||
rules.push({
|
||||
to: [{
|
||||
ipBlock: {
|
||||
@@ -187,21 +192,21 @@ class NetworkPolicyService {
|
||||
except: [
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'127.0.0.0/8'
|
||||
'192.168.0.0/16'
|
||||
]
|
||||
}
|
||||
}]
|
||||
});
|
||||
} else if (policyType === 'NAMESPACE_ONLY') {
|
||||
// Allow only to same namespace
|
||||
rules.push(dnsRuleAllow);
|
||||
rules.push({
|
||||
to: [{
|
||||
podSelector: {}
|
||||
}]
|
||||
});
|
||||
} else if (policyType === 'DENY_ALL') {
|
||||
// Only DNS allowed (already added)
|
||||
// Allow completely nothing
|
||||
}
|
||||
|
||||
return rules;
|
||||
|
||||
Reference in New Issue
Block a user