Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/config-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ function writeAuditArtifacts(
...(config.enableApiProxy && networkConfig.proxyIp ? {
apiProxyIp: networkConfig.proxyIp,
} : {}),
// Include topology peer allow rules so the audit log correctly attributes
// allowed connections to topology-attached containers (e.g. awmg-mcpg:8080)
// rather than misidentifying them as "unknown" or blocked.
topologyPeers: resolveTopologyPeerHosts(config),
});
fs.writeFileSync(
path.join(auditDir, 'policy-manifest.json'),
Expand Down
72 changes: 72 additions & 0 deletions src/squid/policy-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,75 @@ describe('generatePolicyManifest - regex pattern rules', () => {
});
});
});

describe('generatePolicyManifest - topology peers', () => {
const port = 3128;

it('emits no topology peer rules when topologyPeers is undefined', () => {
const manifest = generatePolicyManifest({ domains: ['github.com'], port });
expect(manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-'))).toHaveLength(0);
});

it('emits no topology peer rules when topologyPeers is empty', () => {
const manifest = generatePolicyManifest({ domains: ['github.com'], port, topologyPeers: [] });
expect(manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-'))).toHaveLength(0);
});

it('emits an allow rule for each topology peer', () => {
const manifest = generatePolicyManifest({
domains: ['github.com'],
port,
topologyPeers: ['awmg-mcpg', 'awmg-cli-proxy'],
});

const peerRules = manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-'));
expect(peerRules).toHaveLength(2);

const mcpgRule = peerRules.find(r => r.id === 'allow-topology-peer-awmg-mcpg');
expect(mcpgRule).toBeDefined();
expect(mcpgRule!.action).toBe('allow');
expect(mcpgRule!.protocol).toBe('both');
expect(mcpgRule!.domains).toContain('.awmg-mcpg');

const cliProxyRule = peerRules.find(r => r.id === 'allow-topology-peer-awmg-cli-proxy');
expect(cliProxyRule).toBeDefined();
expect(cliProxyRule!.action).toBe('allow');
expect(cliProxyRule!.domains).toContain('.awmg-cli-proxy');
});

it('places topology peer allow rules before the port-safety deny rules', () => {
const manifest = generatePolicyManifest({
domains: ['github.com'],
port,
topologyPeers: ['awmg-mcpg'],
});

const peerRule = manifest.rules.find(r => r.id === 'allow-topology-peer-awmg-mcpg');
const portSafetyRule = manifest.rules.find(r => r.id === 'deny-unsafe-ports');
expect(peerRule).toBeDefined();
expect(portSafetyRule).toBeDefined();
expect(peerRule!.order).toBeLessThan(portSafetyRule!.order);
});

it('topology peer allow rules are first in the rule list', () => {
const manifest = generatePolicyManifest({
domains: ['github.com'],
port,
topologyPeers: ['awmg-mcpg'],
});

expect(manifest.rules[0].id).toBe('allow-topology-peer-awmg-mcpg');
});

it('keeps sequential rule numbering when topology peers are present', () => {
const manifest = generatePolicyManifest({
domains: ['github.com'],
port,
topologyPeers: ['awmg-mcpg', 'awmg-cli-proxy'],
});

expect(manifest.rules.map(rule => rule.order)).toEqual(
manifest.rules.map((_, index) => index + 1)
);
});
});
8 changes: 7 additions & 1 deletion src/squid/policy-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
addPortSafetyRules,
addProtocolAllowRules,
addRawIpBlockRules,
addTopologyPeerAllowRules,
type PolicyRuleState,
} from './policy-rules/section-builders';

Expand Down Expand Up @@ -55,13 +56,18 @@ export const DANGEROUS_PORTS = [
* enricher skips them and attributes those denials to "unknown".
*/
export function generatePolicyManifest(config: SquidConfig): PolicyManifest {
const { domains, blockedDomains, sslBump, enableHostAccess, allowHostPorts, enableDlp, dnsServers, apiProxyIp } = config;
const { domains, blockedDomains, sslBump, enableHostAccess, allowHostPorts, enableDlp, dnsServers, apiProxyIp, topologyPeers } = config;

// Parse, deduplicate, and group domains by protocol (shared logic with generateSquidConfig)
const { domainsByProto, patternsByProto } = parseDomainConfig(domains);

const state: PolicyRuleState = { rules: [], order: 0 };

// Topology peer allow rules must fire BEFORE the port-safety deny rules so
// that proxy clients reaching e.g. awmg-mcpg:8080 through Squid are allowed
// even though port 8080 is not in Safe_ports. This mirrors the rule ordering
// in generateSquidConfig / generateTopologyPeersSection.
addTopologyPeerAllowRules(state, topologyPeers);
addPortSafetyRules(state);
addApiProxyAllowRules(state, apiProxyIp);
addAllowedIpRules(state, domains);
Expand Down
16 changes: 16 additions & 0 deletions src/squid/policy-rules/section-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ function pushRule(state: PolicyRuleState, rule: Omit<PolicyRule, 'order'>): void
});
}

export function addTopologyPeerAllowRules(state: PolicyRuleState, topologyPeers?: string[]): void {
if (!topologyPeers || topologyPeers.length === 0) return;

for (const peer of topologyPeers) {
const aclName = `topology_peer_${peer.replace(/[^a-zA-Z0-9]/g, '_')}`;
pushRule(state, {
id: `allow-topology-peer-${peer}`,
action: 'allow',
aclName,
protocol: 'both',
domains: [formatDomainForSquid(peer)],
description: `Allow trusted topology peer "${peer}" on any port (network-isolation mode, before Safe_ports deny)`,
});
}
}

export function addPortSafetyRules(state: PolicyRuleState): void {
pushRule(state, {
id: 'deny-unsafe-ports',
Expand Down
Loading