Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ _Avoid_: advanced configuration, app firewall mode
In an App Extended **Full Schema Write**, the App Network Policy Configuration is provided explicitly as either its complete value or `null`; omitting it is invalid.

**Agent Network Policy Configuration**:
The active egress network-policy configuration for an **Agent**, consisting of optional Internet access and detailed target-App rules.
The active egress network-policy configuration for an **Agent**, consisting of optional Internet access, optional **Traefik Access**, and detailed target-App rules.
_Avoid_: agent firewall, app network-policy configuration

In an Agent Extended **Full Schema Write**, the Agent Network Policy Configuration is provided explicitly as either its complete value or `null`; omitting it is invalid.
Expand Down Expand Up @@ -114,6 +114,10 @@ _Avoid_: policy migration, extended upgrade
A configured domain that exposes an **App** through Traefik.
_Avoid_: public app address, ingress rule

**Traefik Access**:
The network-policy permission that lets a workload reach other Apps and Agents through the cluster's Traefik ingress, including through their App Domains and Agent Domains.
_Avoid_: Internet access, cluster egress, public access

**Project Network Graph**:
A visual representation of the effective traffic routes between the Apps and Agents visible from a **Project**, including Traefik ingress permitted by each **App Domain** even when that App has Network Policies disabled.
_Avoid_: network-policy-only graph, deployment topology
Expand Down Expand Up @@ -410,6 +414,9 @@ _Avoid_: manual initial port requirement
- A **Network Policy Mode Migration** replaces an App's active **Simple App Network Policy Configuration** with its equivalent **Extended App Network Policy Configuration**.
- An **Extended App Network Policy Configuration** can have zero or more **App Network Policy Rules** for ingress and zero or more for egress.
- An **App Domain** permits Traefik ingress to its App when Network Policies are active.
- **Traefik Access** is independent of Internet access.
- A workload with **Traefik Access** enabled can reach another workload through its **App Domain** or Agent Domain even when Internet access is disabled.
- **Traefik Access** only applies while Network Policies are active.
- An extended ingress rule identifies an **App Network Policy Rule Source**; an extended egress rule identifies an **App Network Policy Rule Target**.
- An **App Network Policy Rule** may reference an App in another project only when its user has a **Workload Permission** for that App.
- A **Configured Source** belongs to exactly one **App**.
Expand Down
5 changes: 5 additions & 0 deletions docs/adr/0016-traefik-access-egress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Allow Traefik egress as a separate Network Policy flag

Apps and Agents must be able to reach other workloads through their App and Agent Domains, but the Internet Access egress rule excludes the cluster's private CIDRs. Traffic that reaches a node's public ingress address is translated to a Traefik Pod inside a private CIDR and is therefore denied. We add an independent `allowTraefikAccess` flag to the App and Agent Network Policy Configurations that permits egress to the Traefik Pods in `kube-system`, independent of Internet access. It defaults to `false` to preserve existing isolation.

The rule targets the Traefik Pods without port restrictions. Reusing Internet access would widen egress to every private cluster destination, while selecting the Traefik Service IP is not expressible as a Network Policy peer. Explicitly allowing ports `80`/`443` would also fail because the k3s Traefik Pods listen on `8000`/`8443`; Network Policy matches the translated Pod port. Restricting the destination to the Traefik Pods keeps the exception narrowly scoped while covering HTTP and HTTPS entrypoints, including custom ones.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "AppNetworkPolicy" ADD COLUMN "allowTraefikAccess" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "AgentNetworkPolicy" ADD COLUMN "allowTraefikAccess" BOOLEAN NOT NULL DEFAULT false;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ model AgentNetworkPolicy {
agentId String @unique
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
allowInternetAccess Boolean @default(true)
allowTraefikAccess Boolean @default(false)

rules AgentNetworkPolicyRule[]

Expand Down Expand Up @@ -484,6 +485,7 @@ model AppNetworkPolicy {
appId String @unique
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
allowInternetAccess Boolean @default(true)
allowTraefikAccess Boolean @default(false)

rules AppNetworkPolicyRule[]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ function createAgentPayload(
agentFileMounts: [{ containerMountPath: '/workspace/config.json', content: '{}' }],
agentNetworkPolicy: {
allowInternetAccess: true,
allowTraefikAccess: false,
rules: [{ type: 'EGRESS', targetAppId, port: 443, protocol: 'TCP' }],
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ describe('agent.service integration - saveAgentExtendedModel', () => {
agentFileMounts: [{ containerMountPath: '/fresh.txt', content: 'fresh' } as any],
agentNetworkPolicy: {
allowInternetAccess: false,
allowTraefikAccess: true,
rules: [{ targetAppId: targetAppIdB, port: 8443, protocol: 'TCP' }],
} as any,
});
Expand Down Expand Up @@ -153,5 +154,6 @@ describe('agent.service integration - saveAgentExtendedModel', () => {
expect(rules[0].id).not.toBe(staleRule.id);

expect(refreshedPolicy.allowInternetAccess).toBe(false);
expect(refreshedPolicy.allowTraefikAccess).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ describe('app.service integration - subitem ownership guards', () => {
id: sourceApp.id,
appNetworkPolicy: {
allowInternetAccess: false,
allowTraefikAccess: true,
rules: [{
id: rule.id,
targetAppId: targetApp.id,
Expand All @@ -170,6 +171,7 @@ describe('app.service integration - subitem ownership guards', () => {

expect(savedApp.appNetworkPolicy).toMatchObject({
allowInternetAccess: false,
allowTraefikAccess: true,
rules: [expect.objectContaining({ id: rule.id, port: 8443 })],
});
});
Expand All @@ -182,6 +184,7 @@ describe('app.service integration - subitem ownership guards', () => {
id: sourceApp.id,
appNetworkPolicy: {
allowInternetAccess: true,
allowTraefikAccess: false,
rules: [{
targetAppId: sourceApp.id,
targetAgentId: null,
Expand All @@ -208,6 +211,7 @@ describe('app.service integration - subitem ownership guards', () => {
id: sourceApp.id,
appNetworkPolicy: {
allowInternetAccess: true,
allowTraefikAccess: false,
rules: [rule, rule],
},
})).rejects.toThrow('A matching network policy rule already exists.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function AgentNetworkPolicyCard({ agent, readonly }: {
const { openDialog } = useDialog();
const rules = agent.agentNetworkPolicy?.rules ?? [];
const [allowInternetAccess, setAllowInternetAccess] = useState(agent.agentNetworkPolicy?.allowInternetAccess !== false);
const [allowTraefikAccess, setAllowTraefikAccess] = useState(agent.agentNetworkPolicy?.allowTraefikAccess === true);

const handleAllowInternetAccessChange = async (checked: boolean) => {
setAllowInternetAccess(checked);
Expand All @@ -40,6 +41,19 @@ export default function AgentNetworkPolicyCard({ agent, readonly }: {
}
};

const handleAllowTraefikAccessChange = async (checked: boolean) => {
setAllowTraefikAccess(checked);
try {
await Toast.fromAction(() => saveAgentNetworkPolicySettings(
undefined,
{ allowTraefikAccess: checked },
agent.id,
), 'Successfully updated Traefik access setting.');
} catch {
setAllowTraefikAccess(!checked);
}
};

const asyncDeleteRule = async (ruleId: string) => {
const confirm = await openConfirmDialog({
title: "Delete Egress Rule",
Expand Down Expand Up @@ -70,7 +84,7 @@ export default function AgentNetworkPolicyCard({ agent, readonly }: {
<DrawerCardHeader>
<DrawerCardTitle>Network Policy</DrawerCardTitle>
<DrawerCardDescription>
Control whether this agent has general internet access and define specific egress rules to other apps.
Control whether this agent has general internet and Traefik access, and define specific egress rules to other apps.
</DrawerCardDescription>
</DrawerCardHeader>
<DrawerCardContent className="space-y-4">
Expand All @@ -89,6 +103,21 @@ export default function AgentNetworkPolicyCard({ agent, readonly }: {
/>
</div>

<div className="flex items-center justify-between space-x-2 p-4 border rounded-lg">
<div className="space-y-0.5">
<Label htmlFor="allow-traefik-access">Allow Traefik access</Label>
<p className="text-sm text-muted-foreground">
Control whether this agent can reach apps and agents exposed through this cluster&apos;s Traefik ingress.
</p>
</div>
<Switch
id="allow-traefik-access"
disabled={readonly}
checked={allowTraefikAccess}
onCheckedChange={handleAllowTraefikAccessChange}
/>
</div>

<Table>
<TableCaption>{rules.length} Egress Rules</TableCaption>
<TableHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type AppNetworkPolicyRuleSectionProps = {
projects: NetworkPolicyTargetProject[];
internetAccess?: boolean;
onInternetAccessChange?: (checked: boolean) => void;
traefikAccess?: boolean;
onTraefikAccessChange?: (checked: boolean) => void;
};

export default function AppNetworkPolicyRuleSection({
Expand All @@ -49,6 +51,8 @@ export default function AppNetworkPolicyRuleSection({
projects,
internetAccess,
onInternetAccessChange,
traefikAccess,
onTraefikAccessChange,
}: AppNetworkPolicyRuleSectionProps) {
const ingress = direction === 'INGRESS';
const title = ingress ? 'Ingress rules' : 'Egress rules';
Expand Down Expand Up @@ -84,6 +88,22 @@ export default function AppNetworkPolicyRuleSection({
</Card>
)}

{!ingress && traefikAccess !== undefined && onTraefikAccessChange && (
<Card>
<CardContent className="flex items-center justify-between gap-4 p-4">
<div>
<Label>Traefik Access</Label>
<p className="text-sm text-muted-foreground">Allow outgoing connections to apps and agents exposed through this cluster&apos;s Traefik ingress.</p>
</div>
<Switch
checked={traefikAccess}
disabled={readonly}
onCheckedChange={onTraefikAccessChange}
/>
</CardContent>
</Card>
)}

<Table>
<TableHeader>
<TableRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export default function NetworkPolicy({ app, readonly }: { app: AppExtendedModel
setDraft(current => ({ ...current, allowInternetAccess: nextInternet }));
};

const changeTraefikAccess = (nextTraefik: boolean) => {
setDraft(current => ({ ...current, allowTraefikAccess: nextTraefik }));
};

const openRuleDialog = (direction: AppNetworkPolicyDirection) => openDialog(<AppNetworkPolicyRuleDialog
direction={direction}
targets={targets}
Expand All @@ -110,7 +114,7 @@ export default function NetworkPolicy({ app, readonly }: { app: AppExtendedModel
</TabsList>
<TabsContent value="rules" className="mt-5 space-y-8">
<AppNetworkPolicyRuleSection direction="INGRESS" rules={draft.rules.filter(rule => rule.type === 'INGRESS')} readonly={readonly} onAdd={() => openRuleDialog('INGRESS')} onDeleteRule={deleteRule} currentProjectId={app.project.id} projects={projects} />
<AppNetworkPolicyRuleSection direction="EGRESS" rules={draft.rules.filter(rule => rule.type === 'EGRESS')} readonly={readonly} onAdd={() => openRuleDialog('EGRESS')} onDeleteRule={deleteRule} currentProjectId={app.project.id} projects={projects} internetAccess={draft.allowInternetAccess} onInternetAccessChange={changeInternetAccess} />
<AppNetworkPolicyRuleSection direction="EGRESS" rules={draft.rules.filter(rule => rule.type === 'EGRESS')} readonly={readonly} onAdd={() => openRuleDialog('EGRESS')} onDeleteRule={deleteRule} currentProjectId={app.project.id} projects={projects} internetAccess={draft.allowInternetAccess} onInternetAccessChange={changeInternetAccess} traefikAccess={draft.allowTraefikAccess} onTraefikAccessChange={changeTraefikAccess} />
</TabsContent>
<TabsContent value="graph" className="mt-5">
<NetworkPolicyGraph appId={app.id} appName={app.name} appProjectId={app.project.id} rules={graphRules} allowInternetAccess={draft.allowInternetAccess} domainLabels={app.appDomains.map(domain => `${domain.hostname}:${domain.port}`)} projects={projects} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Bot, Globe2, Settings, Trash2 } from 'lucide-react';
import { Bot, Globe2, Network, Settings, Trash2 } from 'lucide-react';
import {
ContextMenu,
ContextMenuContent,
Expand All @@ -28,12 +28,18 @@ export function ProjectNetworkGraphAgentContextMenu({
children,
}: ProjectNetworkGraphAgentContextMenuProps) {
const internetAccessEnabled = agent.agentNetworkPolicy?.allowInternetAccess !== false;
const traefikAccessEnabled = agent.agentNetworkPolicy?.allowTraefikAccess === true;
const [allowsInternetAccess, setAllowsInternetAccess] = useState(internetAccessEnabled);
const [allowsTraefikAccess, setAllowsTraefikAccess] = useState(traefikAccessEnabled);

useEffect(() => {
setAllowsInternetAccess(internetAccessEnabled);
}, [internetAccessEnabled]);

useEffect(() => {
setAllowsTraefikAccess(traefikAccessEnabled);
}, [traefikAccessEnabled]);

const toggleInternetAccess = async () => {
const nextAllowsInternetAccess = !allowsInternetAccess;
setAllowsInternetAccess(nextAllowsInternetAccess);
Expand All @@ -51,6 +57,23 @@ export function ProjectNetworkGraphAgentContextMenu({
}
};

const toggleTraefikAccess = async () => {
const nextAllowsTraefikAccess = !allowsTraefikAccess;
setAllowsTraefikAccess(nextAllowsTraefikAccess);
try {
await Toast.fromAction(
() => saveAgentNetworkPolicySettings(
undefined,
{ allowTraefikAccess: nextAllowsTraefikAccess },
agent.id,
),
`Egress Traefik access ${nextAllowsTraefikAccess ? 'enabled' : 'disabled'}.`,
);
} catch {
setAllowsTraefikAccess(!nextAllowsTraefikAccess);
}
};

return (
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
Expand All @@ -70,6 +93,12 @@ export function ProjectNetworkGraphAgentContextMenu({
<Globe2 />
{allowsInternetAccess ? 'Disable' : 'Enable'} Egress Internet Access
</ContextMenuItem>
<ContextMenuItem
onClick={() => void toggleTraefikAccess()}
>
<Network />
{allowsTraefikAccess ? 'Disable' : 'Enable'} Egress Traefik Access
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive" onClick={onDelete}>
<Trash2 />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import type { ReactNode } from 'react';
import { Box, Globe2, Hammer, Logs, Play, Rocket, RotateCwClock, Settings, Square, Trash2, Zap } from 'lucide-react';
import { Box, Globe2, Hammer, Logs, Network, Play, Rocket, RotateCwClock, Settings, Square, Trash2, Zap } from 'lucide-react';
import {
ContextMenu,
ContextMenuContent,
Expand All @@ -24,6 +24,8 @@ export type ProjectNetworkGraphAppContextMenuProps = {
role?: RolePermissionEnum;
allowInternetAccess: boolean;
onToggleInternetAccess: () => void;
allowTraefikAccess: boolean;
onToggleTraefikAccess: () => void;
onOpenDrawerTab: (tab: 'deployments' | 'logs' | 'backups' | 'settings') => void;
onShowEnvironment: () => void;
onDelete: () => void;
Expand All @@ -35,6 +37,8 @@ export function ProjectNetworkGraphAppContextMenu({
role,
allowInternetAccess,
onToggleInternetAccess,
allowTraefikAccess,
onToggleTraefikAccess,
onDelete,
children,
onOpenDrawerTab,
Expand Down Expand Up @@ -114,6 +118,10 @@ export function ProjectNetworkGraphAppContextMenu({
<Globe2 />
{allowInternetAccess ? 'Disable' : 'Enable'} Egress Internet Access
</ContextMenuItem>
<ContextMenuItem onClick={onToggleTraefikAccess}>
<Network />
{allowTraefikAccess ? 'Disable' : 'Enable'} Egress Traefik Access
</ContextMenuItem>
</>}
{lifecycle.canManage && <ContextMenuItem variant="destructive" onClick={onDelete}>
<Trash2 />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ function ProjectNetworkGraphEditor({
allowInternetAccess: !draft.allowInternetAccess,
}));
}, [updateDraft]);
const toggleTraefikAccess = useCallback((appId: string) => {
updateDraft(appId, draft => ({
...draft,
allowTraefikAccess: !draft.allowTraefikAccess,
}));
}, [updateDraft]);
const saveChanges = async () => {
const changed = Object.values(drafts).filter(draft => !AppNetworkPolicyDraftUtils.equals(draft, baseline[draft.appId]));
if (!changed.length) { return; }
Expand Down Expand Up @@ -310,6 +316,8 @@ function ProjectNetworkGraphEditor({
role: appRole,
allowInternetAccess: draft.allowInternetAccess,
onToggleInternetAccess: () => toggleInternetAccess(app.id),
allowTraefikAccess: draft.allowTraefikAccess,
onToggleTraefikAccess: () => toggleTraefikAccess(app.id),
onOpenDrawerTab: (tab: DrawerTab) => {
drawerSession.openAppTab(app.id, tab);
},
Expand All @@ -330,7 +338,7 @@ function ProjectNetworkGraphEditor({
),
},
};
}), [agents, apps, connectionSourceNodeId, connectionTargetNodeId, deleteLocalAgent, deleteLocalApp, drafts, drawerSession, layout?.edges, layout?.nodes, selectedNodeId, session, toggleInternetAccess]);
}), [agents, apps, connectionSourceNodeId, connectionTargetNodeId, deleteLocalAgent, deleteLocalApp, drafts, drawerSession, layout?.edges, layout?.nodes, selectedNodeId, session, toggleInternetAccess, toggleTraefikAccess]);

const [nodes, setNodes, onNodesChange] = useNodesState(projectedNodes);
useEffect(() => setNodes(projectedNodes), [projectedNodes, setNodes]);
Expand Down
Loading
Loading