@namzu/sandbox 15.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +59 -0
- package/dist/backends/docker/index.d.ts +169 -6
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +480 -84
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/kubernetes/egress-policy.d.ts +193 -102
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/egress-policy.js +321 -146
- package/dist/backends/kubernetes/egress-policy.js.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts +6 -6
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.js +21 -53
- package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -1
- package/dist/index.d.ts +63 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/backends/docker/index.ts +595 -99
- package/src/backends/kubernetes/egress-policy.ts +455 -187
- package/src/backends/kubernetes/per-sandbox-policy.ts +21 -66
- package/src/index.ts +73 -5
|
@@ -596,9 +596,9 @@ export class KubernetesEgressNarrowingUnsupportedError extends Error {
|
|
|
596
596
|
}
|
|
597
597
|
}
|
|
598
598
|
/**
|
|
599
|
-
* The grammar a host refusal
|
|
600
|
-
*
|
|
601
|
-
*
|
|
599
|
+
* The grammar a host refusal closes with: what an `allowedHosts` entry is.
|
|
600
|
+
* Every translation in this module implements it, which is why nothing turns
|
|
601
|
+
* it off — see {@link KubernetesNetworkPolicyHostError}.
|
|
602
602
|
*/
|
|
603
603
|
const HOSTNAME_GRAMMAR_SENTENCE = "Entries are hostnames: 'api.example.com' for one host, '.example.com' for that domain and its subdomains.";
|
|
604
604
|
/**
|
|
@@ -613,26 +613,101 @@ const HOSTNAME_GRAMMAR_SENTENCE = "Entries are hostnames: 'api.example.com' for
|
|
|
613
613
|
* `CiliumNetworkPolicy` carrying one is an object the API server rejects on
|
|
614
614
|
* apply — or, worse, accepts as a name that resolves to nothing, which reads
|
|
615
615
|
* from the outside exactly like a policy that is working.
|
|
616
|
-
*
|
|
617
|
-
* `stateHostnameGrammar` is the one part of the message a caller can turn
|
|
618
|
-
* off, because NOT every refusal can make that claim. It is false for the
|
|
619
|
-
* config-level refusal of a leading-dot entry under `tlsServerNames`, whose
|
|
620
|
-
* body has just said that the entry reaches the object as a literal
|
|
621
|
-
* `matchName` admitting nothing with the option on or off: closing that
|
|
622
|
-
* message with "'.example.com' for that domain and its subdomains" would
|
|
623
|
-
* re-assert, as implemented, the very grammar whose absence is why the entry
|
|
624
|
-
* is refused. Every other refusal keeps the sentence, the expanding branch of
|
|
625
|
-
* the same one included.
|
|
626
616
|
*/
|
|
627
617
|
export class KubernetesNetworkPolicyHostError extends Error {
|
|
628
618
|
host;
|
|
629
619
|
name = 'KubernetesNetworkPolicyHostError';
|
|
630
|
-
constructor(host, reason
|
|
631
|
-
|
|
632
|
-
super(`kubernetes: the allowedHosts entry ${JSON.stringify(host)} is refused: it ${reason}.${grammar} Nothing was written.`);
|
|
620
|
+
constructor(host, reason) {
|
|
621
|
+
super(`kubernetes: the allowedHosts entry ${JSON.stringify(host)} is refused: it ${reason}. ${HOSTNAME_GRAMMAR_SENTENCE} Nothing was written.`);
|
|
633
622
|
this.host = host;
|
|
634
623
|
}
|
|
635
624
|
}
|
|
625
|
+
/** A DNS name, lowercase, no scheme, no port, no wildcard. */
|
|
626
|
+
const DNS_NAME = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/;
|
|
627
|
+
/**
|
|
628
|
+
* The one grammar an `allowedHosts` entry has to satisfy, on EVERY path that
|
|
629
|
+
* emits one.
|
|
630
|
+
*
|
|
631
|
+
* It lives here, beside the class it throws, rather than in the per-sandbox
|
|
632
|
+
* module that first needed it: it is called from {@link
|
|
633
|
+
* buildCiliumEgressManifest} — the single builder both translations go
|
|
634
|
+
* through — so the config-level allowlist and a `setNetworkPolicy` list are
|
|
635
|
+
* refused the SAME entries, by name, with nothing emitted. That is the
|
|
636
|
+
* contract {@link KubernetesNetworkPolicyHostError} already states — it
|
|
637
|
+
* names entries rather than a config field, for exactly this reason — and a
|
|
638
|
+
* grammar only ONE of the two translations enforces is not one the other has:
|
|
639
|
+
* that is how the config-level translation came to pass `'.com'`, `'.'`,
|
|
640
|
+
* `'..example.com'`, `'*'` and an IP literal straight into the emitted
|
|
641
|
+
* `toFQDNs`, where the per-sandbox path refused every one of them.
|
|
642
|
+
*
|
|
643
|
+
* The per-sandbox writer still calls it too — {@link normalizeHost}, which
|
|
644
|
+
* lowercases and then calls this — because there it also CANONICALISES the
|
|
645
|
+
* bytes, before the fence is read and before anything is queued.
|
|
646
|
+
*/
|
|
647
|
+
export function assertUsableHost(entry) {
|
|
648
|
+
// The two reasons the per-sandbox writer's own type check used to give
|
|
649
|
+
// first, kept apart here so an untyped caller meets the same sentence
|
|
650
|
+
// whichever path reached this — see the note on lowercasing below.
|
|
651
|
+
if (typeof entry !== 'string') {
|
|
652
|
+
throw new KubernetesNetworkPolicyHostError(String(entry), 'is not a string');
|
|
653
|
+
}
|
|
654
|
+
if (entry === '') {
|
|
655
|
+
throw new KubernetesNetworkPolicyHostError(entry, 'is empty');
|
|
656
|
+
}
|
|
657
|
+
const bare = entry.startsWith('.') ? entry.slice(1) : entry;
|
|
658
|
+
if (bare === '') {
|
|
659
|
+
throw new KubernetesNetworkPolicyHostError(entry, 'names no domain after its leading dot');
|
|
660
|
+
}
|
|
661
|
+
if (entry.includes('*')) {
|
|
662
|
+
throw new KubernetesNetworkPolicyHostError(entry, "contains a glob; a domain and its subdomains are written with a leading dot ('.example.com'), which becomes matchName plus matchPattern");
|
|
663
|
+
}
|
|
664
|
+
if (!DNS_NAME.test(bare)) {
|
|
665
|
+
throw new KubernetesNetworkPolicyHostError(entry, 'is not a DNS name (a scheme, a path, a port suffix and an IP address all land here; letter case is canonicalised before this check, so it is never the cause)');
|
|
666
|
+
}
|
|
667
|
+
if (bare.length > 253) {
|
|
668
|
+
throw new KubernetesNetworkPolicyHostError(entry, 'is longer than a DNS name may be');
|
|
669
|
+
}
|
|
670
|
+
// A leading-dot entry becomes `matchPattern: '*.<domain>'`, and a
|
|
671
|
+
// single-label domain there is a whole public suffix — `.com`, `.org`.
|
|
672
|
+
// That is not an allowlist entry: it admits every name under a registry
|
|
673
|
+
// the caller does not control. Cilium would match it if it were applied,
|
|
674
|
+
// which is why this is a refusal rather than a lenient pass, and the
|
|
675
|
+
// shipped admission fence refuses the same pattern for the per-sandbox
|
|
676
|
+
// writes it binds (its `matchConditions` scope it to the sandbox host's
|
|
677
|
+
// ServiceAccount, so an operator applying the config-level object is not
|
|
678
|
+
// covered by it — one more reason the refusal has to come from here).
|
|
679
|
+
if (entry.startsWith('.') && !bare.includes('.')) {
|
|
680
|
+
throw new KubernetesNetworkPolicyHostError(entry, "names a whole top-level domain ('.com' means every name under it); a domain entry needs at least two labels, as in '.example.com', and the '*.com' pattern this would emit is one the shipped admission policy refuses for the per-sandbox writes it binds");
|
|
681
|
+
}
|
|
682
|
+
// An IPv4 literal passes the grammar above — every label is digits, and
|
|
683
|
+
// digits are legal in a DNS label. It is still not a hostname: a DNS
|
|
684
|
+
// top-level label is never all-numeric, and Cilium's `matchName` is
|
|
685
|
+
// compared against names the DNS proxy SAW, which an address never is. A
|
|
686
|
+
// policy carrying one is admitted and matches nothing, which reads from
|
|
687
|
+
// outside exactly like a policy that is working.
|
|
688
|
+
const lastLabel = bare.slice(bare.lastIndexOf('.') + 1);
|
|
689
|
+
if (/^[0-9]+$/.test(lastLabel)) {
|
|
690
|
+
throw new KubernetesNetworkPolicyHostError(entry, 'ends in an all-numeric label, so it is an address rather than a hostname; toFQDNs matches names a DNS lookup returned, and an address is never one of them (use config.egress.policy for address-based egress)');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Every entry of one allowlist, judged by {@link assertUsableHost} — called
|
|
695
|
+
* from {@link buildCiliumEgressManifest} before anything is emitted, so BOTH
|
|
696
|
+
* translations refuse the same entries.
|
|
697
|
+
*
|
|
698
|
+
* The entry is lowercased for the DECISION and not for the bytes. The
|
|
699
|
+
* per-sandbox writer canonicalises before it validates (`normalizeHost`), so
|
|
700
|
+
* judging the lowercased form is what makes the two paths agree on WHICH
|
|
701
|
+
* entries are refused — while the emitted `matchName` stays the string the
|
|
702
|
+
* caller wrote, exactly as every release before this one emitted it. This
|
|
703
|
+
* call refuses what no translation can express, not what one of them would
|
|
704
|
+
* spell differently.
|
|
705
|
+
*/
|
|
706
|
+
function assertHostsAreUsable(allowedHosts) {
|
|
707
|
+
for (const host of allowedHosts) {
|
|
708
|
+
assertUsableHost(typeof host === 'string' ? host.toLowerCase() : host);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
636
711
|
/**
|
|
637
712
|
* Every entry in `narrowing.ports`, `narrowing.hostPorts` and
|
|
638
713
|
* `narrowing.tlsPorts` is a port the API server will actually accept, none of
|
|
@@ -932,8 +1007,8 @@ function ciliumPortEntry(port) {
|
|
|
932
1007
|
return { port: String(port), protocol: 'TCP' };
|
|
933
1008
|
}
|
|
934
1009
|
/**
|
|
935
|
-
* The refusal context of the PER-SANDBOX translation —
|
|
936
|
-
*
|
|
1010
|
+
* The refusal context of the PER-SANDBOX translation — `egress.perSandbox`,
|
|
1011
|
+
* whose allowlist comes from a host's own `setNetworkPolicy` call.
|
|
937
1012
|
*
|
|
938
1013
|
* Shared deliberately: the per-sandbox writer refuses a host by this context
|
|
939
1014
|
* before it reads the fence, and {@link buildCiliumEgressManifest} refuses the
|
|
@@ -943,15 +1018,13 @@ function ciliumPortEntry(port) {
|
|
|
943
1018
|
*/
|
|
944
1019
|
export const PER_SANDBOX_NARROWING_REFUSAL = {
|
|
945
1020
|
fieldPath: 'config.egress.perSandbox.narrowing',
|
|
946
|
-
expandsDottedEntries: true,
|
|
947
1021
|
};
|
|
948
1022
|
/**
|
|
949
1023
|
* The refusal context of the config-level translation — `config.egress.policy`
|
|
950
|
-
* and its `config.egress.ciliumNarrowing
|
|
1024
|
+
* and its `config.egress.ciliumNarrowing`.
|
|
951
1025
|
*/
|
|
952
1026
|
export const CONFIG_LEVEL_NARROWING_REFUSAL = {
|
|
953
1027
|
fieldPath: 'config.egress.ciliumNarrowing',
|
|
954
|
-
expandsDottedEntries: false,
|
|
955
1028
|
};
|
|
956
1029
|
/**
|
|
957
1030
|
* Refuse the one allowlist entry a narrowing option cannot express.
|
|
@@ -959,42 +1032,32 @@ export const CONFIG_LEVEL_NARROWING_REFUSAL = {
|
|
|
959
1032
|
* `tlsServerNames` puts the entry on the rule as a TLS server name — one
|
|
960
1033
|
* exact SNI value a handshake presents — and a `.domain` entry is a set of
|
|
961
1034
|
* names no single SNI value means. Either way the emitted object would be
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
1035
|
+
* applied without complaint — nothing refuses an operator's apply: the
|
|
1036
|
+
* shipped admission fence's `matchConditions` scope it to the sandbox host's
|
|
1037
|
+
* ServiceAccount, so the config-level object is not matched by it — read back
|
|
1038
|
+
* deep-equal to what was sent, and deny what the caller asked to allow — the
|
|
1039
|
+
* failure every other refusal in this module exists to prevent. Refused
|
|
1040
|
+
* rather than translated another way,
|
|
965
1041
|
* because both other ways are guesses: dropping the subdomains silently
|
|
966
1042
|
* narrows what the caller asked for, and a wildcard SNI value is not
|
|
967
1043
|
* something the Cilium versions these manifests are written against are
|
|
968
1044
|
* known here to match — an SNI that matches nothing denies just as
|
|
969
1045
|
* completely, and more quietly.
|
|
970
1046
|
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
* not one — the config-level `.domain` entry denies the domain and every
|
|
986
|
-
* subdomain with or without the option (a pre-existing defect of this
|
|
987
|
-
* translation, deferred to its own change) — so it is not offered here.
|
|
988
|
-
*
|
|
989
|
-
* `context` is WHICH translation the refusal is being spelled for, as one
|
|
990
|
-
* value — see {@link HostsFitNarrowingContext}. A reader has to be sent to
|
|
991
|
-
* the field they actually set, and the sentence they are sent by has to be
|
|
992
|
-
* true of the translation they are on, and both follow from that one fact:
|
|
993
|
-
* passed separately, an expanding caller gets the per-sandbox remedy attached
|
|
994
|
-
* to the config-level field name, a message whose only repair is a field the
|
|
995
|
-
* entry never came from. Called from BOTH — the translation itself, so every
|
|
996
|
-
* caller is covered, and the per-sandbox writer, which refuses earlier still,
|
|
997
|
-
* before it reads the fence.
|
|
1047
|
+
* One sentence, because one entry now means one thing: both translations turn
|
|
1048
|
+
* `.domain` into `matchName: domain` PLUS `matchPattern: '*.domain'` — see
|
|
1049
|
+
* {@link ciliumFqdnEntries} — and no single SNI value means that pair.
|
|
1050
|
+
* `domain` alone denies every subdomain the pattern admits, and the entry as
|
|
1051
|
+
* written is not a name any handshake presents at all.
|
|
1052
|
+
*
|
|
1053
|
+
* `context` is the field the refusal sends the reader to — see
|
|
1054
|
+
* {@link HostsFitNarrowingContext}. A reader has to be sent to the field they
|
|
1055
|
+
* actually set: passed as a loose string, an expanding caller was told to
|
|
1056
|
+
* repair `config.egress.ciliumNarrowing` with the remedy that belongs to
|
|
1057
|
+
* `config.egress.perSandbox.narrowing`, a message whose only repair is a field
|
|
1058
|
+
* the entry never came from. Called from BOTH — the translation itself, so
|
|
1059
|
+
* every caller is covered, and the per-sandbox writer, which refuses earlier
|
|
1060
|
+
* still, before it reads the fence.
|
|
998
1061
|
*/
|
|
999
1062
|
export function assertHostsFitNarrowing(allowedHosts, narrowing, context) {
|
|
1000
1063
|
if (narrowing?.tlsServerNames !== true)
|
|
@@ -1002,16 +1065,7 @@ export function assertHostsFitNarrowing(allowedHosts, narrowing, context) {
|
|
|
1002
1065
|
for (const host of allowedHosts) {
|
|
1003
1066
|
if (!host.startsWith('.'))
|
|
1004
1067
|
continue;
|
|
1005
|
-
throw new KubernetesNetworkPolicyHostError(host, context.
|
|
1006
|
-
? `names a domain and its subdomains while ${context.fieldPath}.tlsServerNames is on; a TLS server name is one exact SNI value a handshake presents, and this entry becomes a name plus a '*.domain' pattern, which no single value means — list the exact hosts, or leave tlsServerNames off for a domain list`
|
|
1007
|
-
: `names a domain and its subdomains while ${context.fieldPath}.tlsServerNames is on; a TLS server name is one exact SNI value a handshake presents, and this translation does not expand a leading-dot entry — it reaches the object as the literal matchName ${JSON.stringify(host)}, which no DNS answer carries — so the entry admits nothing as written, and serverNames on top of it is a second, independent denial. List the exact hosts this policy should allow; leaving tlsServerNames off does not repair the entry here`,
|
|
1008
|
-
// The closing grammar rides the same input as the sentence, and for
|
|
1009
|
-
// the same reason: the unexpanded body has just said this entry
|
|
1010
|
-
// admits nothing as written, so a tail asserting that
|
|
1011
|
-
// `.example.com` means the domain and its subdomains would state,
|
|
1012
|
-
// as implemented, the grammar the refusal exists because this
|
|
1013
|
-
// translation does not apply to it.
|
|
1014
|
-
{ stateHostnameGrammar: context.expandsDottedEntries });
|
|
1068
|
+
throw new KubernetesNetworkPolicyHostError(host, `names a domain and its subdomains while ${context.fieldPath}.tlsServerNames is on; a TLS server name is one exact SNI value a handshake presents, and this entry becomes a name plus a '*.domain' pattern, which no single value means — list the exact hosts, or leave tlsServerNames off for a domain list`);
|
|
1015
1069
|
}
|
|
1016
1070
|
}
|
|
1017
1071
|
/**
|
|
@@ -1037,18 +1091,25 @@ function narrowedHostPorts(host, narrowing) {
|
|
|
1037
1091
|
* host's TLS ports and a plain entry for whatever is left, when
|
|
1038
1092
|
* `tlsServerNames` is on.
|
|
1039
1093
|
*
|
|
1040
|
-
* `
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1094
|
+
* `fqdns` is {@link ciliumFqdnEntries} of the host's allowlist entry, passed
|
|
1095
|
+
* in rather than derived here because the entry and the host are not always
|
|
1096
|
+
* the same string: `.example.com` becomes `example.com` plus
|
|
1097
|
+
* `*.example.com`. It is REQUIRED, with no `[{ matchName: host }]` default —
|
|
1098
|
+
* a caller that omitted it would emit the entry unexpanded, which is the
|
|
1099
|
+
* exact object this translation stopped producing.
|
|
1100
|
+
*
|
|
1101
|
+
* `serverNames` carries the allowlist ENTRY as written while the `toFQDNs`
|
|
1102
|
+
* half carries the expansion of it. A TLS server name is one exact SNI value
|
|
1103
|
+
* a handshake presents, and a `.example.com` entry — whose `toFQDNs` half is
|
|
1104
|
+
* `example.com` PLUS `*.example.com` — has no single SNI value meaning that
|
|
1105
|
+
* set: `example.com` would deny every subdomain the pattern admits, and
|
|
1106
|
+
* `.example.com` is not a name any handshake ever presents. So
|
|
1107
|
+
* {@link assertHostsFitNarrowing} refuses that one combination before this
|
|
1108
|
+
* rule is built, from `buildCiliumEgressManifest` for every caller and from
|
|
1109
|
+
* the per-sandbox writer before it reads the fence — which is why this
|
|
1110
|
+
* function can still take `host` and the entry as one string.
|
|
1050
1111
|
*/
|
|
1051
|
-
function narrowedHostFqdnRule(host, narrowing, fqdns
|
|
1112
|
+
function narrowedHostFqdnRule(host, narrowing, fqdns) {
|
|
1052
1113
|
const ports = narrowedHostPorts(host, narrowing);
|
|
1053
1114
|
if (ports === undefined)
|
|
1054
1115
|
return { toFQDNs: fqdns };
|
|
@@ -1072,18 +1133,20 @@ function narrowedHostFqdnRule(host, narrowing, fqdns = [{ matchName: host }]) {
|
|
|
1072
1133
|
/**
|
|
1073
1134
|
* One allowlist entry, expanded to the `toFQDNs` entries it means.
|
|
1074
1135
|
*
|
|
1075
|
-
* `SandboxNetworkPolicy.allowedHosts`'s own grammar, which the SDK states
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
* name
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1136
|
+
* `SandboxNetworkPolicy.allowedHosts`'s own grammar, which the SDK states —
|
|
1137
|
+
* `packages/sdk/src/types/sandbox/index.ts`'s `allowedHosts` — and which
|
|
1138
|
+
* `src/egress/allowlist.ts` implements for the docker backend: `api.example.com`
|
|
1139
|
+
* is that host, and `.example.com` is the domain AND its subdomains. Cilium's
|
|
1140
|
+
* `matchName` is an exact name and does not match across a `.`, so the domain
|
|
1141
|
+
* form needs the name plus a `matchPattern` — `*.example.com` alone would
|
|
1142
|
+
* admit `a.example.com` and not `example.com` itself.
|
|
1143
|
+
*
|
|
1144
|
+
* Used by BOTH translations. The config-level one used to emit a leading-dot
|
|
1145
|
+
* entry verbatim instead, which no DNS answer carries and so denied the
|
|
1146
|
+
* domain and every subdomain it was asked to allow, after a create that
|
|
1147
|
+
* reported success — but "the config-level bytes are pinned" was never a
|
|
1148
|
+
* contract the pinned bytes honoured, and one entry has to mean one thing on
|
|
1149
|
+
* every backend.
|
|
1087
1150
|
*/
|
|
1088
1151
|
export function ciliumFqdnEntries(entry) {
|
|
1089
1152
|
if (!entry.startsWith('.'))
|
|
@@ -1092,12 +1155,18 @@ export function ciliumFqdnEntries(entry) {
|
|
|
1092
1155
|
return [{ matchName: domain }, { matchPattern: `*.${domain}` }];
|
|
1093
1156
|
}
|
|
1094
1157
|
/**
|
|
1095
|
-
* The DNS-visibility rule narrowed to
|
|
1096
|
-
* plus the host under every search
|
|
1158
|
+
* The DNS-visibility rule narrowed to the names an allowlist entry admits:
|
|
1159
|
+
* an exact `matchName` per allowed host plus the host under every search
|
|
1160
|
+
* suffix, and — for an entry that admits subdomains — the `matchPattern` that
|
|
1161
|
+
* admits them, under the bare name and under every suffix as well. Replaces
|
|
1097
1162
|
* {@link CILIUM_DNS_VISIBILITY_RULE}'s `matchPattern: '*'`. See
|
|
1098
1163
|
* {@link KubernetesCiliumDnsNarrowing}.
|
|
1164
|
+
*
|
|
1165
|
+
* Both halves follow from {@link ciliumFqdnEntries}: the DNS proxy has to see
|
|
1166
|
+
* exactly what the `toFQDNs` rule admits, or the half that learns addresses
|
|
1167
|
+
* from a lookup never sees one the other half allows.
|
|
1099
1168
|
*/
|
|
1100
|
-
function narrowedDnsVisibilityRule(allowedHosts, fallbackNamespace, dnsNames
|
|
1169
|
+
function narrowedDnsVisibilityRule(allowedHosts, fallbackNamespace, dnsNames) {
|
|
1101
1170
|
const namespace = dnsNames.namespace ?? fallbackNamespace;
|
|
1102
1171
|
const clusterDomain = dnsNames.clusterDomain ?? 'cluster.local';
|
|
1103
1172
|
const suffixes = [
|
|
@@ -1110,11 +1179,8 @@ function narrowedDnsVisibilityRule(allowedHosts, fallbackNamespace, dnsNames, ex
|
|
|
1110
1179
|
for (const entry of allowedHosts) {
|
|
1111
1180
|
// A `.domain` entry resolves through its subdomains as well, so the
|
|
1112
1181
|
// DNS proxy has to be allowed to SEE those lookups or `toFQDNs` never
|
|
1113
|
-
// learns the addresses they resolve to.
|
|
1114
|
-
|
|
1115
|
-
// false there and `host === entry`, so this loop is what it always
|
|
1116
|
-
// was.
|
|
1117
|
-
const wildcard = expandDomains && entry.startsWith('.');
|
|
1182
|
+
// learns the addresses they resolve to.
|
|
1183
|
+
const wildcard = entry.startsWith('.');
|
|
1118
1184
|
const host = wildcard ? entry.slice(1) : entry;
|
|
1119
1185
|
matchNames.push({ matchName: host });
|
|
1120
1186
|
if (wildcard)
|
|
@@ -1125,9 +1191,9 @@ function narrowedDnsVisibilityRule(allowedHosts, fallbackNamespace, dnsNames, ex
|
|
|
1125
1191
|
// with fewer dots than the cluster's `ndots` tries the search
|
|
1126
1192
|
// suffixes FIRST, and a lookup the DNS proxy refuses is not an
|
|
1127
1193
|
// NXDOMAIN the resolver walks past — it can fail the whole
|
|
1128
|
-
// resolution. An
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1194
|
+
// resolution. An entry that admits subdomains needs the pattern
|
|
1195
|
+
// under each suffix too, or `a.example.com` fails on its first
|
|
1196
|
+
// search-suffix attempt under a rule that allows it.
|
|
1131
1197
|
if (wildcard)
|
|
1132
1198
|
matchNames.push({ matchPattern: `*.${host}.${suffix}` });
|
|
1133
1199
|
}
|
|
@@ -1143,29 +1209,40 @@ function narrowedDnsVisibilityRule(allowedHosts, fallbackNamespace, dnsNames, ex
|
|
|
1143
1209
|
};
|
|
1144
1210
|
}
|
|
1145
1211
|
export function buildCiliumEgressManifest(options) {
|
|
1146
|
-
const { narrowing, allowedHosts,
|
|
1147
|
-
// Before anything is emitted
|
|
1212
|
+
const { narrowing, allowedHosts, refusalContext } = options;
|
|
1213
|
+
// Before anything is emitted, and for EVERY caller — which is the point:
|
|
1214
|
+
// an entry this backend will not translate is one whose object would be
|
|
1215
|
+
// either rejected on apply or, worse, accepted as a name that matches
|
|
1216
|
+
// nothing while the create reports success. The config-level allowlist
|
|
1217
|
+
// used to reach this builder unvalidated, so `['.com']`, `['.']`,
|
|
1218
|
+
// `['..example.com']`, `['*']` and an address were each emitted into
|
|
1219
|
+
// `toFQDNs` — the first three as a `matchPattern` no fence covers on that
|
|
1220
|
+
// path (the shipped one is scoped to the sandbox host's ServiceAccount),
|
|
1221
|
+
// which turned a fail-closed no-op into a silent grant of every name under
|
|
1222
|
+
// a public suffix. See {@link assertHostsAreUsable}.
|
|
1223
|
+
assertHostsAreUsable(allowedHosts);
|
|
1224
|
+
// A leading-dot entry under `tlsServerNames`
|
|
1148
1225
|
// would become `serverNames: ['.domain']` — not a name any handshake
|
|
1149
1226
|
// presents — and `['domain']` would deny every subdomain the `toFQDNs`
|
|
1150
|
-
// half of the same rule admits; the object
|
|
1151
|
-
//
|
|
1152
|
-
// would ever report it.
|
|
1227
|
+
// half of the same rule admits; the object goes through with nothing
|
|
1228
|
+
// objecting to it, and reads back deep-equal to what was sent, so nothing
|
|
1229
|
+
// downstream would ever report it. (On the config-level path nothing
|
|
1230
|
+
// objects to it at all: the shipped fence is scoped to the sandbox host's
|
|
1231
|
+
// ServiceAccount and an operator's apply is not matched by it.)
|
|
1153
1232
|
//
|
|
1154
1233
|
// What this call guarantees, per caller shape: the per-sandbox writer
|
|
1155
1234
|
// refuses the same hosts earlier still, by name and by the same context
|
|
1156
1235
|
// (`per-sandbox-policy.ts`), and this call covers them again if that check
|
|
1157
1236
|
// is ever reached later or skipped; the config-level translation has no
|
|
1158
1237
|
// earlier check of its own and relies on this one entirely; and a direct
|
|
1159
|
-
// call to this function is covered here too, whatever
|
|
1238
|
+
// call to this function is covered here too, whatever context it passes.
|
|
1160
1239
|
//
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
|
|
1167
|
-
// `perSandbox.narrowing`.
|
|
1168
|
-
assertHostsFitNarrowing(allowedHosts, narrowing, expandDomains ? PER_SANDBOX_NARROWING_REFUSAL : CONFIG_LEVEL_NARROWING_REFUSAL);
|
|
1240
|
+
// The context decides ONE thing, because one thing is left to decide —
|
|
1241
|
+
// which field the refusal names. It is required rather than derived, since
|
|
1242
|
+
// nothing here can see the caller's config, and derived from the bytes is
|
|
1243
|
+
// exactly how an expanding caller came to be sent to the config-level
|
|
1244
|
+
// field for a repair that only exists under `perSandbox.narrowing`.
|
|
1245
|
+
assertHostsFitNarrowing(allowedHosts, narrowing, refusalContext);
|
|
1169
1246
|
// Unnarrowed is the exact shape every release before #490 emitted — kept
|
|
1170
1247
|
// as its own branch, untouched, rather than folded into the narrowed one
|
|
1171
1248
|
// with every option defaulted off, so the byte-identical guarantee does
|
|
@@ -1173,12 +1250,11 @@ export function buildCiliumEgressManifest(options) {
|
|
|
1173
1250
|
const narrowed = ciliumNarrowingIsActive(narrowing);
|
|
1174
1251
|
const activeDns = narrowed ? activeDnsNarrowing(narrowing.dnsNames) : undefined;
|
|
1175
1252
|
const dnsRule = activeDns !== undefined
|
|
1176
|
-
? narrowedDnsVisibilityRule(allowedHosts, options.namespace, activeDns
|
|
1253
|
+
? narrowedDnsVisibilityRule(allowedHosts, options.namespace, activeDns)
|
|
1177
1254
|
: CILIUM_DNS_VISIBILITY_RULE;
|
|
1178
|
-
const entriesFor = (entry) => expandDomains ? ciliumFqdnEntries(entry) : [{ matchName: entry }];
|
|
1179
1255
|
const hostRules = narrowed
|
|
1180
|
-
? allowedHosts.map((host) => narrowedHostFqdnRule(host, narrowing,
|
|
1181
|
-
: [{ toFQDNs: allowedHosts.flatMap(
|
|
1256
|
+
? allowedHosts.map((host) => narrowedHostFqdnRule(host, narrowing, ciliumFqdnEntries(host)))
|
|
1257
|
+
: [{ toFQDNs: allowedHosts.flatMap(ciliumFqdnEntries) }];
|
|
1182
1258
|
return {
|
|
1183
1259
|
kind: 'CiliumNetworkPolicy',
|
|
1184
1260
|
policyKind: options.policyKind,
|
|
@@ -1211,6 +1287,7 @@ function buildCiliumNetworkPolicy(target, policyKind, allowedHosts, narrowing) {
|
|
|
1211
1287
|
allowedHosts,
|
|
1212
1288
|
policyKind,
|
|
1213
1289
|
...(narrowing !== undefined ? { narrowing } : {}),
|
|
1290
|
+
refusalContext: CONFIG_LEVEL_NARROWING_REFUSAL,
|
|
1214
1291
|
});
|
|
1215
1292
|
}
|
|
1216
1293
|
/**
|
|
@@ -1302,8 +1379,16 @@ export class KubernetesEgressPolicyMismatchError extends Error {
|
|
|
1302
1379
|
* `../docker/index.ts`'s `assertNetworkCarriesThePolicy`, which inspects the
|
|
1303
1380
|
* daemon's own `{{.Internal}}` flag instead of trusting a network's name.
|
|
1304
1381
|
*
|
|
1305
|
-
* Checks
|
|
1306
|
-
*
|
|
1382
|
+
* Checks four things, each named separately in a mismatch so an operator sees
|
|
1383
|
+
* which one to fix:
|
|
1384
|
+
* - the object carries NO `specs` list. A `CiliumNetworkPolicy` carries
|
|
1385
|
+
* EITHER one `spec` or a `specs` list and a rule in either one enforces,
|
|
1386
|
+
* while every check below reads `spec` alone — so an object carrying both
|
|
1387
|
+
* would be compared on half of what it enforces. This is refused and not
|
|
1388
|
+
* read, because a comparison that accepts rules it never looked at is the
|
|
1389
|
+
* one answer this function must not give; the shipped admission fence
|
|
1390
|
+
* refuses a `specs` list for the same reason. The translation never emits
|
|
1391
|
+
* one, so its presence is drift and not an alternative spelling;
|
|
1307
1392
|
* - the selector (`podSelector` for `NetworkPolicy`, `endpointSelector` for
|
|
1308
1393
|
* `CiliumNetworkPolicy`) carries the expected template label:
|
|
1309
1394
|
* - `NetworkPolicy` additionally declares `policyTypes` including
|
|
@@ -1336,6 +1421,24 @@ export async function verifyEgressPolicyApplied(client, translated, signal) {
|
|
|
1336
1421
|
const expectedSpec = translated.manifest.spec;
|
|
1337
1422
|
const actualSpec = resource?.spec ?? {};
|
|
1338
1423
|
const selectorKey = translated.kind === 'CiliumNetworkPolicy' ? 'endpointSelector' : 'podSelector';
|
|
1424
|
+
// First, and before anything is compared: an object carrying a `specs`
|
|
1425
|
+
// list is refused rather than read. Everything below reads `spec`, and a
|
|
1426
|
+
// `CiliumNetworkPolicy` rule in a `specs` entry enforces exactly as one in
|
|
1427
|
+
// `spec` does — so comparing `spec` and reporting a match would be
|
|
1428
|
+
// accepting rules this function never looked at, which is the one answer it
|
|
1429
|
+
// must not give. `readCiliumEgressPolicies` reads both spellings and
|
|
1430
|
+
// `decideEgressUnion` refuses a `specs` entry that widens; this is the
|
|
1431
|
+
// other half of the same rule, for the callers that run this check ALONE —
|
|
1432
|
+
// `verify: 'named-object-only'`, and the per-sandbox read-back — and it is
|
|
1433
|
+
// what makes the named-object exemption in `decideEgressUnion` sound rather
|
|
1434
|
+
// than merely narrower. The translation never emits a `specs` list, and the
|
|
1435
|
+
// shipped admission fence refuses one for the same reason.
|
|
1436
|
+
const actualSpecs = resource?.specs;
|
|
1437
|
+
if (actualSpecs !== undefined && actualSpecs !== null) {
|
|
1438
|
+
throw new KubernetesEgressPolicyMismatchError(translated.kind, path, `specs is ${JSON.stringify(actualSpecs)}, and the translation never emits one — ${translated.kind === 'CiliumNetworkPolicy'
|
|
1439
|
+
? 'a CiliumNetworkPolicy carries EITHER one spec or a specs list, and a rule in either one enforces'
|
|
1440
|
+
: 'a NetworkPolicy has no specs field at all'}, while this check reads spec.${selectorKey}${translated.kind === 'NetworkPolicy' ? ', spec.policyTypes' : ''} and spec.egress and nothing else — so anything a specs list enforces is egress this comparison never read`);
|
|
1441
|
+
}
|
|
1339
1442
|
if (!isDeepStrictEqual(actualSpec[selectorKey], expectedSpec[selectorKey])) {
|
|
1340
1443
|
throw new KubernetesEgressPolicyMismatchError(translated.kind, path, `spec.${selectorKey} is ${JSON.stringify(actualSpec[selectorKey])}, expected ${JSON.stringify(expectedSpec[selectorKey])} — the label every Sandbox this backend creates carries`);
|
|
1341
1444
|
}
|
|
@@ -1627,6 +1730,7 @@ export function egressAllowance(translated) {
|
|
|
1627
1730
|
permitsEverything: false,
|
|
1628
1731
|
permitsNothing: false,
|
|
1629
1732
|
policyKind: translated.policyKind,
|
|
1733
|
+
namedObject: { kind: translated.kind, name: translated.name },
|
|
1630
1734
|
};
|
|
1631
1735
|
}
|
|
1632
1736
|
const destinations = [];
|
|
@@ -1663,6 +1767,7 @@ export function egressAllowance(translated) {
|
|
|
1663
1767
|
permitsEverything,
|
|
1664
1768
|
permitsNothing: rules.length === 0,
|
|
1665
1769
|
policyKind: translated.policyKind,
|
|
1770
|
+
namedObject: { kind: translated.kind, name: translated.name },
|
|
1666
1771
|
};
|
|
1667
1772
|
}
|
|
1668
1773
|
// ---------------------------------------------------------------------------
|
|
@@ -2148,6 +2253,31 @@ function unreadableEgressPolicy(kind, name, detail) {
|
|
|
2148
2253
|
unreadable: detail,
|
|
2149
2254
|
};
|
|
2150
2255
|
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Is this the object `config.egress` named — see
|
|
2258
|
+
* {@link EgressPolicyDocument.namedObject}.
|
|
2259
|
+
*
|
|
2260
|
+
* Read off the allowance rather than passed in, so every caller of
|
|
2261
|
+
* {@link readCoreEgressPolicies}/{@link readCiliumEgressPolicies} that built
|
|
2262
|
+
* its allowance with {@link egressAllowance} gets the marking without having
|
|
2263
|
+
* to remember a second argument, and the readers stay a function of "(items,
|
|
2264
|
+
* this pod, what the translation allows)".
|
|
2265
|
+
*
|
|
2266
|
+
* For a core `NetworkPolicy` the name is the whole answer. For a
|
|
2267
|
+
* `CiliumNetworkPolicy` it is not: the CRD carries EITHER one `spec` or a
|
|
2268
|
+
* `specs` list, `verifyEgressPolicyApplied` reads the former and refuses an
|
|
2269
|
+
* object carrying the latter, so the reader marks only the document built
|
|
2270
|
+
* from `item.spec` and lets a `specs` entry be judged by the union rule like
|
|
2271
|
+
* any other object's rules — see {@link EgressPolicyDocument.namedObject}.
|
|
2272
|
+
*
|
|
2273
|
+
* Deliberately NOT applied to an unreadable document: an object at the named
|
|
2274
|
+
* name that could not be read keeps its `not-evaluable` verdict, which refuses
|
|
2275
|
+
* — a fail-closed answer for a shape whose exact comparison already refused it
|
|
2276
|
+
* earlier on the create path.
|
|
2277
|
+
*/
|
|
2278
|
+
function isNamedObject(allowance, kind, name) {
|
|
2279
|
+
return allowance.namedObject.kind === kind && allowance.namedObject.name === name;
|
|
2280
|
+
}
|
|
2151
2281
|
/** Every core `NetworkPolicy` in the list, reduced to {@link EgressPolicyDocument}. */
|
|
2152
2282
|
export function readCoreEgressPolicies(items, target, allowance) {
|
|
2153
2283
|
return items.map((item, index) => {
|
|
@@ -2175,6 +2305,7 @@ export function readCoreEgressPolicies(items, target, allowance) {
|
|
|
2175
2305
|
return {
|
|
2176
2306
|
kind: 'NetworkPolicy',
|
|
2177
2307
|
name,
|
|
2308
|
+
...(isNamedObject(allowance, 'NetworkPolicy', name) ? { namedObject: true } : {}),
|
|
2178
2309
|
selects: matchesLabelSelector(spec.podSelector, target.podLabels),
|
|
2179
2310
|
enforcesEgress,
|
|
2180
2311
|
// An `egress` block under a `policyTypes` that leaves Egress out is
|
|
@@ -2197,30 +2328,47 @@ export function readCiliumEgressPolicies(items, target, allowance) {
|
|
|
2197
2328
|
}
|
|
2198
2329
|
// The CRD carries EITHER one `spec` or a `specs` list, and a rule in
|
|
2199
2330
|
// either enforces. Reading only `spec` would miss a whole policy.
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2331
|
+
//
|
|
2332
|
+
// The two spellings are NOT equal where the named-object exemption is
|
|
2333
|
+
// concerned — see {@link EgressPolicyDocument.namedObject}. The exact
|
|
2334
|
+
// comparison the exemption leans on reads `spec` and refuses an object
|
|
2335
|
+
// carrying a `specs` list, so `spec` is the one document that earns the
|
|
2336
|
+
// marking; a `specs` entry is read as an ordinary policy's rules and is
|
|
2337
|
+
// judged by the union rule, which is what refuses one that widens.
|
|
2338
|
+
const named = isNamedObject(allowance, 'CiliumNetworkPolicy', name);
|
|
2339
|
+
const specDocuments = [];
|
|
2340
|
+
if (item.spec !== undefined && item.spec !== null) {
|
|
2341
|
+
specDocuments.push({ spec: item.spec, isTheSpec: true });
|
|
2342
|
+
}
|
|
2203
2343
|
const more = readList(item.specs);
|
|
2204
2344
|
if (more === 'unreadable') {
|
|
2205
2345
|
documents.push(unreadableEgressPolicy('CiliumNetworkPolicy', name, 'a specs that is not a list of rule specs'));
|
|
2206
2346
|
continue;
|
|
2207
2347
|
}
|
|
2208
2348
|
for (const spec of more ?? [])
|
|
2209
|
-
|
|
2210
|
-
if (
|
|
2349
|
+
specDocuments.push({ spec, isTheSpec: false });
|
|
2350
|
+
if (specDocuments.length === 0) {
|
|
2211
2351
|
documents.push(unreadableEgressPolicy('CiliumNetworkPolicy', name, 'neither a spec nor a specs list'));
|
|
2212
2352
|
continue;
|
|
2213
2353
|
}
|
|
2214
|
-
for (const spec of
|
|
2215
|
-
const document = readCiliumEgressRuleSpec(spec, name, identity, allowance);
|
|
2354
|
+
for (const { spec, isTheSpec } of specDocuments) {
|
|
2355
|
+
const document = readCiliumEgressRuleSpec(spec, name, identity, allowance, named && isTheSpec);
|
|
2216
2356
|
if (document !== undefined)
|
|
2217
2357
|
documents.push(document);
|
|
2218
2358
|
}
|
|
2219
2359
|
}
|
|
2220
2360
|
return documents;
|
|
2221
2361
|
}
|
|
2222
|
-
/**
|
|
2223
|
-
|
|
2362
|
+
/**
|
|
2363
|
+
* One `spec`/`specs` entry. `undefined` when it is node-scoped — see below.
|
|
2364
|
+
*
|
|
2365
|
+
* `isTheNamedSpec` is true only for the document built from the named
|
|
2366
|
+
* object's own `spec` — the one {@link verifyEgressPolicyApplied} compared to
|
|
2367
|
+
* the translation. A `specs` entry never is; see
|
|
2368
|
+
* {@link EgressPolicyDocument.namedObject} for why that distinction is
|
|
2369
|
+
* load-bearing rather than bookkeeping.
|
|
2370
|
+
*/
|
|
2371
|
+
function readCiliumEgressRuleSpec(spec, name, identity, allowance, isTheNamedSpec) {
|
|
2224
2372
|
const unreadable = (detail) => unreadableEgressPolicy('CiliumNetworkPolicy', name, detail);
|
|
2225
2373
|
if (!isRecord(spec))
|
|
2226
2374
|
return unreadable('a rule spec that is not an object');
|
|
@@ -2248,6 +2396,7 @@ function readCiliumEgressRuleSpec(spec, name, identity, allowance) {
|
|
|
2248
2396
|
return {
|
|
2249
2397
|
kind: 'CiliumNetworkPolicy',
|
|
2250
2398
|
name,
|
|
2399
|
+
...(isTheNamedSpec ? { namedObject: true } : {}),
|
|
2251
2400
|
selects: matchesLabelSelector(spec.endpointSelector, identity, ciliumSelectorKey),
|
|
2252
2401
|
enforcesEgress,
|
|
2253
2402
|
// `egressDeny` rules are not read: a deny rule can only narrow what
|
|
@@ -2266,6 +2415,15 @@ function readCiliumEgressRuleSpec(spec, name, identity, allowance) {
|
|
|
2266
2415
|
* translation is the finding however many narrower ones sit beside it, and a
|
|
2267
2416
|
* pod no policy default-denies has no egress boundary at all whatever the
|
|
2268
2417
|
* named object says.
|
|
2418
|
+
*
|
|
2419
|
+
* The named object's `spec` is the one document whose rules are not judged
|
|
2420
|
+
* here — see {@link EgressPolicyDocument.namedObject} — and it was compared to
|
|
2421
|
+
* the translation by {@link verifyEgressPolicyApplied} on the same create path
|
|
2422
|
+
* just before. Its SELECTOR and its egress-scoping still decide, which is what
|
|
2423
|
+
* keeps "nothing bounds this pod" answerable for a deployment whose only
|
|
2424
|
+
* policy is that one; and a document built from a `specs` entry is judged here
|
|
2425
|
+
* like any other object's, because the exempting comparison refuses an object
|
|
2426
|
+
* carrying a `specs` list rather than reading one.
|
|
2269
2427
|
*/
|
|
2270
2428
|
export function decideEgressUnion(documents, allowance) {
|
|
2271
2429
|
const examined = [];
|
|
@@ -2298,27 +2456,39 @@ export function decideEgressUnion(documents, allowance) {
|
|
|
2298
2456
|
undecided ??= entry;
|
|
2299
2457
|
continue;
|
|
2300
2458
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2459
|
+
// The named object's `spec` rules were just compared to the translation
|
|
2460
|
+
// by `verifyEgressPolicyApplied`, field by field, which is the one
|
|
2461
|
+
// judgement about them that a `toFQDNs`-by-`matchName` allowance cannot
|
|
2462
|
+
// reproduce — see {@link EgressPolicyDocument.namedObject}. So the union
|
|
2463
|
+
// rule does not judge them; judging them here is what made the check
|
|
2464
|
+
// refuse the object it had just told an operator to apply. Everything
|
|
2465
|
+
// below still applies, which is how a pod whose only policy is the named
|
|
2466
|
+
// one is still known to be in egress default-deny. Only that `spec`
|
|
2467
|
+
// document carries the marking: a `specs` entry is judged here like any
|
|
2468
|
+
// other rules.
|
|
2469
|
+
if (document.namedObject !== true) {
|
|
2470
|
+
const beyondRule = document.rules.find((rule) => rule.beyond === true);
|
|
2471
|
+
if (beyondRule !== undefined) {
|
|
2472
|
+
const entry = {
|
|
2473
|
+
...base,
|
|
2474
|
+
verdict: 'widens-egress',
|
|
2475
|
+
...(beyondRule.detail !== undefined ? { detail: beyondRule.detail } : {}),
|
|
2476
|
+
};
|
|
2477
|
+
examined.push(entry);
|
|
2478
|
+
widening ??= entry;
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
const unknownRule = document.rules.find((rule) => rule.beyond === 'unknown');
|
|
2482
|
+
if (unknownRule !== undefined) {
|
|
2483
|
+
const entry = {
|
|
2484
|
+
...base,
|
|
2485
|
+
verdict: 'not-evaluable',
|
|
2486
|
+
...(unknownRule.detail !== undefined ? { detail: unknownRule.detail } : {}),
|
|
2487
|
+
};
|
|
2488
|
+
examined.push(entry);
|
|
2489
|
+
undecided ??= entry;
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2322
2492
|
}
|
|
2323
2493
|
if (!document.enforcesEgress) {
|
|
2324
2494
|
examined.push({
|
|
@@ -2428,7 +2598,12 @@ function formatEgressRemedy(refusal, unread) {
|
|
|
2428
2598
|
* Verify-not-trust, widened from one object to the union: list the
|
|
2429
2599
|
* namespace's policies, evaluate every one that selects this pod against the
|
|
2430
2600
|
* configured translation, and refuse unless nothing lets out more than
|
|
2431
|
-
* `config.egress` says.
|
|
2601
|
+
* `config.egress` says. The named object's `spec` document is the one
|
|
2602
|
+
* exception, and it was compared to the translation by
|
|
2603
|
+
* {@link verifyEgressPolicyApplied} on this same create path just before this
|
|
2604
|
+
* one — see {@link EgressPolicyDocument.namedObject}: a Cilium object that
|
|
2605
|
+
* carries a `specs` list is refused there outright, so a `specs` entry is
|
|
2606
|
+
* judged HERE, like any other object's rules.
|
|
2432
2607
|
*
|
|
2433
2608
|
* Runs beside the ingress check on every create path — before the POST for a
|
|
2434
2609
|
* directly created Sandbox, where the labels are known and a refusal leaves
|