Skip to content

Commit c0638c7

Browse files
authored
Let the API reach Intelligence and sign-in when a NetworkPolicy is on (#257)
1 parent 951d20f commit c0638c7

4 files changed

Lines changed: 103 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@ the membership row like the pin — so one person reading does not clear anybody
1717

1818
The deployment gains one nullable column, via migration `0019`.
1919

20+
### The API can reach Intelligence and sign-in when a NetworkPolicy is on
21+
22+
`networkPolicy.enabled` wrote a rule for the API server that named DNS, the database and the Bots'
23+
computers, and nothing on 443. On a cluster that enforces policy the server could therefore reach
24+
neither CopilotKit Intelligence, nor an identity provider, nor a Bot: nobody could sign in and no
25+
conversation ran. Two of the five shipped `ci/` targets turn the policy on, and on GKE enforcement is
26+
the default and cannot be switched off.
27+
28+
Nothing said so. The pod passed every probe and stayed Ready, because `/health` answers from a
29+
literal, so the first evidence was a timeout to a hostname that read as the internet being down.
30+
31+
The API now reaches HTTP and HTTPS everywhere outside the cluster's private ranges, in every
32+
`computers.mode` rather than only `sandbox`, cut by the same exception list the computers' own policy
33+
uses. It still cannot address another pod, a node, or a cloud metadata endpoint.
34+
35+
`mode: sandbox` had been working only because a rule meant for the Kubernetes API server carried no
36+
destination and so permitted everything. That rule now covers the API server alone, and
37+
`networkPolicy.kubernetesApiCidr` narrows it to your cluster's service range; left empty it stays as
38+
it was, because a chart cannot know that range.
2039

2140
### A finished turn shows the page it opened, not the one open now
2241

‎charts/openbot/templates/networkpolicy.yaml‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ Off by default, because a NetworkPolicy on a cluster with no CNI that enforces o
77
that silently does nothing, and on a cluster that does enforce one a wrong rule is an outage. A
88
deployment that turns this on is saying it knows which of the two it has.
99

10-
Egress deliberately allows DNS and the database, and nothing else without being asked: a Bot's
11-
computer reaching the open internet is the computers' own policy, not the API's.
10+
Egress allows DNS, the database, the computers, and HTTP and HTTPS to everywhere that is not the
11+
cluster's own private network. THAT LAST ONE IS NOT A CONCESSION, it is what this pod does: sign-in
12+
goes to an identity provider, every conversation goes to Intelligence, and every run goes to a Bot
13+
at an address somebody registered. All three are hostnames rather than CIDRs, and a NetworkPolicy
14+
cannot match a hostname, so there is no narrower rule to write. Leaving it out did not fence the API
15+
off, it stopped the product working, and only in `computers.mode: sandbox` did a rule meant for the
16+
Kubernetes API server quietly cover for it.
17+
18+
The private ranges stay cut out by exception, the way the computers' policy does it, so this is
19+
still a pod that cannot address another pod, a node, or a cloud metadata endpoint.
1220
*/}}
1321
apiVersion: networking.k8s.io/v1
1422
kind: NetworkPolicy
@@ -62,9 +70,43 @@ spec:
6270
- port: 4100
6371
protocol: TCP
6472
{{- end }}
73+
{{- /*
74+
Intelligence, the identity provider, and every Bot: all of them, and all outside the cluster.
75+
76+
Written as an exception list rather than as a destination list because the destinations are
77+
hostnames the deployment configures and a NetworkPolicy matches addresses. The same shape the
78+
computers' policy below already uses, for the same reason.
79+
*/}}
80+
- to:
81+
- ipBlock:
82+
cidr: 0.0.0.0/0
83+
except:
84+
# The cluster and everything else on the private network, including the database.
85+
- 10.0.0.0/8
86+
- 172.16.0.0/12
87+
- 192.168.0.0/16
88+
# Link-local, which is where every cloud keeps the endpoint that hands out credentials.
89+
- 169.254.0.0/16
90+
ports:
91+
- port: 80
92+
protocol: TCP
93+
- port: 443
94+
protocol: TCP
6595
{{- if eq .Values.computers.mode "sandbox" }}
66-
{{- /* The API server, which is where a per-Bot computer is asked for. */}}
67-
- ports:
96+
{{- /*
97+
The Kubernetes API server, which is where a per-Bot computer is asked for.
98+
99+
Its own rule because it sits on the private network the rule above cuts out, so nothing else
100+
here reaches it. Unscoped unless a deployment says otherwise: the API server answers on a
101+
ClusterIP from the service range, and a chart cannot know that range at template time. Name it
102+
in `networkPolicy.kubernetesApiCidr` and this narrows to it.
103+
*/}}
104+
- {{- with .Values.networkPolicy.kubernetesApiCidr }}
105+
to:
106+
- ipBlock:
107+
cidr: {{ . }}
108+
{{- end }}
109+
ports:
68110
- port: 443
69111
protocol: TCP
70112
- port: 6443

‎charts/openbot/values.yaml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,18 @@ httpRoute:
365365
networkPolicy:
366366
enabled: false
367367
# Where the API may reach out to. A deployment with a managed database adds its CIDR here.
368+
#
369+
# It already reaches HTTP and HTTPS everywhere outside the cluster's private ranges, because that
370+
# is where Intelligence, sign-in and the Bots are. This is for anything on the private side.
368371
extraEgress: []
369372
extraIngress: []
373+
# `computers.mode: sandbox` only. The service range the Kubernetes API server answers on, so the
374+
# rule that lets the API ask for a Bot's computer can name it instead of being left open.
375+
#
376+
# Empty means unscoped, which is the only thing a chart can do by default: the range is the
377+
# cluster's, not the release's. `kubectl get svc kubernetes -o jsonpath='{.spec.clusterIP}'` shows
378+
# which one yours is on; on EKS it is usually 172.20.0.0/16, on GKE and kubeadm 10.96.0.0/12.
379+
kubernetesApiCidr: ""
370380
# Where a Bot's computer may reach beyond the public internet. A deployment whose Bots must reach
371381
# an internal site adds it here, one address at a time, rather than reopening the private ranges.
372382
computerExtraEgress: []

‎scripts/check-rendered-chart.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,41 @@ for (const [name, keys] of written) {
127127
}
128128
}
129129

130+
/**
131+
* A policy that fences the API off from the services it cannot work without.
132+
*
133+
* The same question as the Secret one above, asked of the other thing a render can be internally
134+
* wrong about: this chart requires CopilotKit Intelligence and an identity provider, reaches both
135+
* over HTTPS at hostnames, and also writes the rule that says where the API may go. Those two had
136+
* never been compared. The server's egress named DNS, the database and the computers, so on any
137+
* cluster that enforces policy nobody could sign in and no conversation ran — and the pod stayed
138+
* Ready throughout, because `/health` answers from a literal.
139+
*
140+
* Asked of the rendered object rather than the template, because the rule that covered for this was
141+
* conditional on `computers.mode` and only one mode ever had it.
142+
*/
143+
const serverPolicy = documents.find(
144+
(document) =>
145+
/^kind:\s*NetworkPolicy\s*$/m.test(document) &&
146+
/app\.kubernetes\.io\/component:\s*server/.test(document),
147+
);
148+
if (serverPolicy) {
149+
const egress = serverPolicy.split(/^\s{2}egress:\s*$/m)[1] ?? "";
150+
if (!/port:\s*443\b/.test(egress)) {
151+
problems.push(
152+
"The server's NetworkPolicy has no egress on 443, so the API cannot reach Intelligence or an identity provider. Nothing would report it: /health answers from a literal and every probe reads it.",
153+
);
154+
}
155+
}
156+
130157
if (problems.length > 0) {
131158
for (const problem of problems) console.error(`::error::${problem}`);
132159
process.exit(1);
133160
}
134161

135162
console.log(
136163
`${documents.length} objects, ${demands.length} secret keys demanded, and every required one is written.` +
164+
(serverPolicy ? " The server's egress reaches 443." : "") +
137165
(skippedOptional > 0
138166
? ` ${skippedOptional} optional key${skippedOptional === 1 ? " was" : "s were"} not checked.`
139167
: ""),

0 commit comments

Comments
 (0)