@emilia-protocol/gate 0.9.5 → 0.10.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +190 -0
  3. package/README.md +75 -16
  4. package/action-control-manifest.js +26 -0
  5. package/adapters/_kit.js +47 -5
  6. package/adapters/aws.js +15 -4
  7. package/adapters/github-demo.mjs +25 -22
  8. package/adapters/github.js +1 -1
  9. package/adapters/jira.js +1 -0
  10. package/adapters/linear.js +1 -0
  11. package/adapters/salesforce.js +1 -0
  12. package/adapters/stripe.js +1 -1
  13. package/adapters/supabase.js +26 -4
  14. package/adapters/vercel.js +33 -4
  15. package/aec-execution.js +1 -3
  16. package/breakglass.js +285 -51
  17. package/control-plane.js +341 -0
  18. package/coverage.js +722 -0
  19. package/custody-demo.mjs +13 -3
  20. package/demo.mjs +9 -3
  21. package/deploy/helm/README.md +16 -8
  22. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  23. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  24. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  25. package/deploy/helm/emilia-gate/values.yaml +18 -2
  26. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  27. package/deploy/helm/emilia-gate-service/README.md +81 -0
  28. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  29. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  30. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  31. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  32. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  33. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  34. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  35. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  36. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  37. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  38. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  39. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  40. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  41. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  42. package/deploy/sql/001-runtime.sql +707 -0
  43. package/deploy/terraform/README.md +24 -14
  44. package/deploy/terraform/main.tf +59 -0
  45. package/deploy/terraform/service/README.md +81 -0
  46. package/deploy/terraform/service/main.tf +718 -0
  47. package/deploy/terraform/service/outputs.tf +19 -0
  48. package/deploy/terraform/service/tests/validate.sh +24 -0
  49. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  50. package/deploy/terraform/service/variables.tf +459 -0
  51. package/deploy/terraform/service/versions.tf +10 -0
  52. package/deploy/terraform/variables.tf +95 -2
  53. package/deployment-attestation.js +248 -0
  54. package/eg1-conformance.js +46 -12
  55. package/enterprise.js +6 -2
  56. package/ep-assure.mjs +3 -2
  57. package/evidence-postgres.js +357 -0
  58. package/evidence.js +10 -3
  59. package/execution-binding.js +141 -26
  60. package/index.js +556 -115
  61. package/key-registry.js +51 -19
  62. package/mcp.js +4 -2
  63. package/network-witness.js +500 -0
  64. package/package.json +25 -5
  65. package/reliance-kernel.js +5 -6
  66. package/reliance-packet.js +43 -10
  67. package/reports/assurance-package.js +45 -19
  68. package/reports/reperform.js +78 -18
  69. package/reports/underwriter.js +1 -1
  70. package/settlement.js +300 -0
  71. package/store-postgres.js +14 -0
  72. package/store.js +26 -5
  73. package/strict-json.js +86 -0
  74. package/witness-postgres.js +97 -0
package/custody-demo.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
- * EMILIA Gate — production custody demo. Run: node custody-demo.mjs
2
+ * EMILIA Gate — issuer custody reference demo. Run: node custody-demo.mjs
3
3
  *
4
4
  * Shows the three custody controls a serious buyer asks for:
5
5
  * 1. ROTATION — a new issuer key takes over without breaking the old one.
6
6
  * 2. REVOCATION — a compromised issuer key is rejected immediately, live.
7
7
  * 3. RETENTION — the evidence log classifies hot/cold/expired + exports.
8
+ * Issuer keys use the live registry path. Approver keys are embedded only to
9
+ * keep this reference demo self-contained; production Gate deployments pin
10
+ * approver keys or provide their own assurance verifier.
8
11
  * @license Apache-2.0
9
12
  */
10
13
  import { createGate, createKeyRegistry, createEg1Harness } from './index.js';
@@ -14,7 +17,14 @@ const SEL = { protocol: 'mcp', tool: 'release_payment' };
14
17
  const k1 = createEg1Harness({ idPrefix: 'k1' });
15
18
  const k2 = createEg1Harness({ idPrefix: 'k2' });
16
19
  const registry = createKeyRegistry([{ kid: 'issuer-1', key: k1.publicKey }]);
17
- const gate = createGate({ manifest: DEFAULT_GATE_MANIFEST, keyRegistry: registry });
20
+ const gate = createGate({
21
+ manifest: DEFAULT_GATE_MANIFEST,
22
+ keyRegistry: registry,
23
+ rpId: k1.rpId,
24
+ allowedOrigins: k1.allowedOrigins,
25
+ allowEmbeddedApproverKeys: true,
26
+ allowEphemeralStore: true,
27
+ });
18
28
 
19
29
  const G = (s) => `\x1b[32m${s}\x1b[0m`; const R = (s) => `\x1b[31m${s}\x1b[0m`;
20
30
  const line = (s) => console.log(s);
@@ -24,7 +34,7 @@ async function pay(label, harness) {
24
34
  }
25
35
 
26
36
  line('='.repeat(64));
27
- line(' EMILIA Gate — production custody: rotate, revoke, retain');
37
+ line(' EMILIA Gate — custody reference: rotate, revoke, retain');
28
38
  line('='.repeat(64));
29
39
  await pay('1. payout, issuer-1 (current) ', k1);
30
40
  registry.revoke('issuer-1');
package/demo.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * EMILIA Gate — end-to-end demo. Run: node demo.mjs
3
- * Shows the Trusted Action Firewall refusing and allowing a consequential
3
+ * Shows the Consequence Firewall refusing and allowing a consequential
4
4
  * action, defeating replay and tampering, and emitting a verifiable audit log.
5
5
  * @license Apache-2.0
6
6
  */
@@ -35,13 +35,19 @@ const receipt = (outcome, extra = {}) => {
35
35
  return { '@version': 'EP-RECEIPT-v1', payload, signature: { algorithm: 'Ed25519', value: crypto.sign(null, Buffer.from(canon(payload), 'utf8'), privateKey).toString('base64url') } };
36
36
  };
37
37
 
38
- const gate = createTrustedActionFirewall({ trustedKeys: [pub] });
38
+ const gate = createTrustedActionFirewall({
39
+ trustedKeys: [pub],
40
+ rpId: 'emiliaprotocol.ai',
41
+ allowedOrigins: ['https://www.emiliaprotocol.ai'],
42
+ allowEmbeddedApproverKeys: true,
43
+ allowEphemeralStore: true,
44
+ });
39
45
  const PAY = { protocol: 'mcp', tool: 'release_payment' };
40
46
  const line = (s) => console.log(s);
41
47
  const r = (o) => o.allow ? `\x1b[32mALLOW\x1b[0m` : `\x1b[31mREFUSE ${o.status}\x1b[0m (${o.reason})`;
42
48
 
43
49
  line('='.repeat(64));
44
- line(' EMILIA Gate — Trusted Action Firewall (release_payment, critical)');
50
+ line(' EMILIA Gate — Consequence Firewall (release_payment, critical)');
45
51
  line('='.repeat(64));
46
52
  line(`\n 1. read_status (not guarded) -> ${r(await gate.check({ selector: { protocol: 'mcp', tool: 'read_status' } }))}`);
47
53
  line(` 2. release_payment, no receipt -> ${r(await gate.check({ selector: PAY }))}`);
@@ -1,7 +1,11 @@
1
1
  <!-- SPDX-License-Identifier: Apache-2.0 -->
2
2
  # EMILIA Gate — reference Helm chart (`emilia-gate`)
3
3
 
4
- **Contract:** `EP-GATE-HELM-v1` · **Status: reference chart, experimental, BYOC.**
4
+ **Contract:** `EP-GATE-HELM-v1` · **Status: deprecated, experimental, BYOC.**
5
+
6
+ This legacy chart is retained only for compatibility. It can use per-process
7
+ in-memory consumption and evidence state. New deployments should use
8
+ `emilia-gate-service`, whose runtime refuses missing durable adapters.
5
9
 
6
10
  A reference Kubernetes deployment of the [EMILIA Gate](../../README.md) — the
7
11
  Trusted Action Firewall. Deny-by-default enforcement for consequential machine
@@ -36,6 +40,9 @@ public image yet — build and push your own image of the gate service.
36
40
  `capabilities: drop [ALL]`, `seccompProfile: RuntimeDefault`,
37
41
  `automountServiceAccountToken: false`. Loosen deliberately via values, not
38
42
  accidentally.
43
+ 4. **Single replica by default**: `replicaCount` defaults to `1`. Rendering more
44
+ than one replica fails unless both shared consumption and evidence backends
45
+ are wired through complete Secret-backed `sharedState` references.
39
46
 
40
47
  ## Quick start
41
48
 
@@ -50,7 +57,7 @@ kubectl -n emilia create secret generic emilia-gate-issuer-keys \
50
57
  helm install gate ./emilia-gate \
51
58
  --namespace emilia --create-namespace \
52
59
  --set image.repository=YOUR_REGISTRY/emilia-gate \
53
- --set image.tag=0.9.5 \
60
+ --set image.tag=0.10.0 \
54
61
  --set issuerKeys.existingSecret=emilia-gate-issuer-keys
55
62
 
56
63
  # 3. (Optional) expose /metrics + ServiceMonitor stub (needs Prometheus Operator).
@@ -82,8 +89,9 @@ Secrets/ConfigMaps there rather than inlining values.
82
89
 
83
90
  | Value | Default | Meaning |
84
91
  | ----- | ------- | ------- |
85
- | `replicaCount` | `2` | Gate replicas. NOTE: replay defense across replicas needs a shared consumption store (see `createDurableConsumptionStore` in `packages/gate/store.js`); configure the backend via `gate.extraEnv`. |
86
- | `image.repository` / `image.tag` | `ghcr.io/emilia-protocol/gate` / chart `appVersion` | BYOC image. |
92
+ | `replicaCount` | `1` | Legacy Gate replicas. Values above one are refused without both Secret-backed shared-state references. |
93
+ | `sharedState.consumption` / `sharedState.evidence` | empty | Each requires `envName`, `existingSecret`, and `secretKey`; both are mandatory above one replica. |
94
+ | `image.repository` / `image.tag` | — **required** / chart `appVersion` | Operator-built BYOC image; no official public image is implied. |
87
95
  | `issuerKeys.existingSecret` | — **required** | Name of your pre-created Secret with pinned issuer keys. |
88
96
  | `issuerKeys.secretKey` | `issuer-keys.json` | Key inside that Secret. |
89
97
  | `manifest.inline` | subset of default packs | Action-risk manifest rendered to the ConfigMap. Replace with your org's manifest. |
@@ -96,10 +104,10 @@ Secrets/ConfigMaps there rather than inlining values.
96
104
 
97
105
  - **Experimental.** Not battle-tested; `helm lint`-clean is not a security
98
106
  review. Read the rendered output (`helm template`) before applying.
99
- - The default in-memory consumption store and evidence sink are per-pod. For
100
- fleets (`replicaCount > 1`) wire a shared/durable backend or replayed
101
- receipts on another pod are only caught by receipt freshness, not
102
- consumption.
107
+ - The default in-memory consumption store and evidence sink are per-pod. The
108
+ chart now refuses fleets unless both shared backend Secret references are
109
+ complete. That render-time check proves wiring exists, not that a custom
110
+ legacy image correctly implements the adapters; prefer `emilia-gate-service`.
103
111
  - The ServiceMonitor is a **stub**: no TLS/auth on the scrape endpoint —
104
112
  adapt it to your monitoring stack.
105
113
  - No Ingress/NetworkPolicy/HPA templates on purpose: the gate is an internal
@@ -15,11 +15,12 @@ description: >-
15
15
  Reference chart, experimental, BYOC. Issuer keys are referenced from an
16
16
  existing Secret and are never rendered by this chart.
17
17
  type: application
18
+ deprecated: true
18
19
  # Chart version (the packaging), independent of the gate's app version.
19
20
  version: 0.1.0
20
21
  # Version of @emilia-protocol/gate this chart tracks; used as the default
21
22
  # image tag when .Values.image.tag is empty.
22
- appVersion: "0.9.5"
23
+ appVersion: "0.10.0"
23
24
  kubeVersion: ">=1.23.0-0"
24
25
  home: https://emiliaprotocol.ai
25
26
  sources:
@@ -35,5 +36,6 @@ annotations:
35
36
  emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
36
37
  emiliaprotocol.ai/maturity: experimental
37
38
  emiliaprotocol.ai/deployment-model: byoc
39
+ emiliaprotocol.ai/replacement: emilia-gate-service
38
40
  artifacthub.io/prerelease: "true"
39
41
  artifacthub.io/license: Apache-2.0
@@ -6,8 +6,9 @@ EMILIA Gate — Trusted Action Firewall (chart {{ .Chart.Name }}-{{ .Chart.Versi
6
6
  Image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}
7
7
  Replicas: {{ .Values.replicaCount }}
8
8
 
9
- STATUS: reference chart — EXPERIMENTAL, BYOC. Review the rendered manifests
10
- before trusting production traffic to them.
9
+ STATUS: DEPRECATED reference chart — EXPERIMENTAL, BYOC. Prefer the
10
+ emilia-gate-service chart. Review the rendered manifests before trusting
11
+ production traffic to them.
11
12
 
12
13
  1. Issuer keys (already enforced at render time — this release referenced the
13
14
  existing Secret "{{ .Values.issuerKeys.existingSecret }}", key
@@ -7,6 +7,42 @@
7
7
  {{- $name := default .Chart.Name .Values.nameOverride }}
8
8
  {{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
9
9
  {{- $manifestConfigMap := default $fullname .Values.manifest.existingConfigMap }}
10
+ {{- $consumption := .Values.sharedState.consumption }}
11
+ {{- $evidence := .Values.sharedState.evidence }}
12
+ {{- $consumptionAny := or (ne $consumption.envName "") (ne $consumption.existingSecret "") (ne $consumption.secretKey "") }}
13
+ {{- $evidenceAny := or (ne $evidence.envName "") (ne $evidence.existingSecret "") (ne $evidence.secretKey "") }}
14
+ {{- $consumptionConfigured := and (ne $consumption.envName "") (ne $consumption.existingSecret "") (ne $consumption.secretKey "") }}
15
+ {{- $evidenceConfigured := and (ne $evidence.envName "") (ne $evidence.existingSecret "") (ne $evidence.secretKey "") }}
16
+ {{- $reservedEnv := list "NODE_ENV" "EP_GATE_PORT" "EP_GATE_LOG_LEVEL" "EP_GATE_EVIDENCE_STRICT" "EP_GATE_MANIFEST_PATH" "EP_GATE_METRICS_ENABLED" "EP_GATE_ISSUER_KEYS" }}
17
+ {{- if and $consumptionAny (not $consumptionConfigured) }}
18
+ {{- fail "sharedState.consumption requires envName, existingSecret, and secretKey together" }}
19
+ {{- end }}
20
+ {{- if and $evidenceAny (not $evidenceConfigured) }}
21
+ {{- fail "sharedState.evidence requires envName, existingSecret, and secretKey together" }}
22
+ {{- end }}
23
+ {{- if and $consumptionConfigured (not (regexMatch "^[A-Za-z_][A-Za-z0-9_]*$" $consumption.envName)) }}
24
+ {{- fail "sharedState.consumption.envName must be a valid environment variable name" }}
25
+ {{- end }}
26
+ {{- if and $evidenceConfigured (not (regexMatch "^[A-Za-z_][A-Za-z0-9_]*$" $evidence.envName)) }}
27
+ {{- fail "sharedState.evidence.envName must be a valid environment variable name" }}
28
+ {{- end }}
29
+ {{- if and $consumptionConfigured (has $consumption.envName $reservedEnv) }}
30
+ {{- fail "sharedState.consumption.envName must not override a chart-managed environment variable" }}
31
+ {{- end }}
32
+ {{- if and $evidenceConfigured (has $evidence.envName $reservedEnv) }}
33
+ {{- fail "sharedState.evidence.envName must not override a chart-managed environment variable" }}
34
+ {{- end }}
35
+ {{- if and $consumptionConfigured $evidenceConfigured (eq $consumption.envName $evidence.envName) }}
36
+ {{- fail "sharedState.consumption.envName and sharedState.evidence.envName must be distinct" }}
37
+ {{- end }}
38
+ {{- range $extra := .Values.gate.extraEnv }}
39
+ {{- if or (has $extra.name $reservedEnv) (and $consumptionConfigured (eq $extra.name $consumption.envName)) (and $evidenceConfigured (eq $extra.name $evidence.envName)) }}
40
+ {{- fail (printf "gate.extraEnv name %s duplicates a chart-managed or shared-state environment variable" $extra.name) }}
41
+ {{- end }}
42
+ {{- end }}
43
+ {{- if and (gt (int .Values.replicaCount) 1) (not (and $consumptionConfigured $evidenceConfigured)) }}
44
+ {{- fail "deprecated emilia-gate refuses replicaCount > 1 without complete Secret-backed sharedState.consumption and sharedState.evidence references; prefer emilia-gate-service" }}
45
+ {{- end }}
10
46
  apiVersion: apps/v1
11
47
  kind: Deployment
12
48
  metadata:
@@ -21,6 +57,8 @@ metadata:
21
57
  helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
22
58
  emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
23
59
  emiliaprotocol.ai/maturity: experimental
60
+ emiliaprotocol.ai/deprecated: "true"
61
+ emiliaprotocol.ai/replacement: emilia-gate-service
24
62
  spec:
25
63
  replicas: {{ .Values.replicaCount }}
26
64
  selector:
@@ -55,7 +93,7 @@ spec:
55
93
  {{- toYaml .Values.podSecurityContext | nindent 8 }}
56
94
  containers:
57
95
  - name: gate
58
- image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
96
+ image: "{{ required "image.repository is required; build and push a BYOC image before rendering this deprecated chart" .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
59
97
  imagePullPolicy: {{ .Values.image.pullPolicy }}
60
98
  securityContext:
61
99
  {{- toYaml .Values.containerSecurityContext | nindent 12 }}
@@ -86,6 +124,22 @@ spec:
86
124
  secretKeyRef:
87
125
  name: {{ required "issuerKeys.existingSecret is required. This chart NEVER renders key material — create the Secret out-of-band (see README.md) and set issuerKeys.existingSecret to its name." .Values.issuerKeys.existingSecret }}
88
126
  key: {{ .Values.issuerKeys.secretKey }}
127
+ {{- if $consumptionConfigured }}
128
+ - name: {{ $consumption.envName }}
129
+ valueFrom:
130
+ secretKeyRef:
131
+ name: {{ $consumption.existingSecret }}
132
+ key: {{ $consumption.secretKey }}
133
+ optional: false
134
+ {{- end }}
135
+ {{- if $evidenceConfigured }}
136
+ - name: {{ $evidence.envName }}
137
+ valueFrom:
138
+ secretKeyRef:
139
+ name: {{ $evidence.existingSecret }}
140
+ key: {{ $evidence.secretKey }}
141
+ optional: false
142
+ {{- end }}
89
143
  {{- with .Values.gate.extraEnv }}
90
144
  {{- toYaml . | nindent 12 }}
91
145
  {{- end }}
@@ -13,12 +13,15 @@
13
13
  # capabilities, no service-account token). Loosen deliberately, not
14
14
  # accidentally.
15
15
 
16
- replicaCount: 2
16
+ # This deprecated chart uses in-memory state unless a BYOC image consumes both
17
+ # Secret-backed shared-state references below. One replica is the only safe
18
+ # default. Prefer the emilia-gate-service chart for production deployments.
19
+ replicaCount: 1
17
20
 
18
21
  image:
19
22
  # BYOC: build and push your own image of the gate service; there is no
20
23
  # official public image while the chart is experimental.
21
- repository: ghcr.io/emilia-protocol/gate
24
+ repository: ""
22
25
  # Defaults to the chart's appVersion when empty.
23
26
  tag: ""
24
27
  pullPolicy: IfNotPresent
@@ -43,6 +46,19 @@ gate:
43
46
  # EnvVar objects — supports valueFrom for your own Secret/ConfigMap refs).
44
47
  extraEnv: []
45
48
 
49
+ # Legacy multi-replica guard. These references are rendered as environment
50
+ # variables for a BYOC image that implements the corresponding durable adapters.
51
+ # Setting replicaCount > 1 is refused unless BOTH references are complete.
52
+ sharedState:
53
+ consumption:
54
+ envName: ""
55
+ existingSecret: ""
56
+ secretKey: ""
57
+ evidence:
58
+ envName: ""
59
+ existingSecret: ""
60
+ secretKey: ""
61
+
46
62
  # ---------------------------------------------------------------------------
47
63
  # Issuer keys — REQUIRED, referenced, never rendered.
48
64
  # Create the Secret yourself, e.g.:
@@ -0,0 +1,11 @@
1
+ apiVersion: v2
2
+ name: emilia-gate-service
3
+ description: BYOC deployment chart for the EMILIA Gate service
4
+ type: application
5
+ version: 0.2.0
6
+ appVersion: "0.2.0"
7
+ kubeVersion: ">=1.27.0-0"
8
+ annotations:
9
+ emiliaprotocol.ai/deployment-model: byoc
10
+ emiliaprotocol.ai/maturity: reference
11
+ artifacthub.io/license: Apache-2.0
@@ -0,0 +1,81 @@
1
+ # EMILIA Gate service Helm chart
2
+
3
+ This chart is a BYOC reference deployment for `apps/gate-service`. It does not
4
+ name or imply an official published image. Build `Dockerfile.gate`, push the
5
+ result to a registry you control, and pass an immutable digest where possible.
6
+
7
+ The chart intentionally refuses to render with its default values. Supply:
8
+
9
+ - `image.repository` and exactly one of `image.digest` or `image.tag`;
10
+ - `configuration.existingSecret` containing `gate.config.mjs` and
11
+ `migrate.mjs`;
12
+ - `secrets.postgres.existingSecret` with the database URL key;
13
+ - `migrations.postgres.existingSecret` with a distinct DDL-capable login URL;
14
+ - `secrets.apiToken.existingSecret` with the Gate action-API bearer token;
15
+ - optionally, `secrets.kms.existingSecret` when the operator config uses a KMS-backed extension;
16
+ - `secrets.issuerRoots.existingSecret` with the pinned issuer-root set.
17
+
18
+ The chart never renders a Kubernetes `Secret`. Secret names and keys are wired
19
+ with non-optional references. The migration hook receives the operator config
20
+ and a mandatory Postgres reference distinct from the runtime Secret. The
21
+ service receives the config and runtime
22
+ references. The config module must satisfy the durable store contracts in
23
+ `apps/gate-service/README.md`. The chart does not substitute an in-memory
24
+ adapter or invent a persistence schema on the operator's behalf.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ helm upgrade --install emilia-gate \
30
+ packages/gate/deploy/helm/emilia-gate-service \
31
+ --namespace emilia-gate --create-namespace \
32
+ --set-string image.repository=registry.example.com/security/emilia-gate-service \
33
+ --set-string image.digest=sha256:REPLACE_WITH_IMAGE_DIGEST \
34
+ --set-string configuration.existingSecret=emilia-gate-configuration \
35
+ --set-string secrets.postgres.existingSecret=emilia-gate-postgres \
36
+ --set-string migrations.postgres.existingSecret=emilia-gate-postgres-migrate \
37
+ --set-string migrations.postgres.key=database-url \
38
+ --set-string secrets.apiToken.existingSecret=emilia-gate-api-token \
39
+ --set-string secrets.issuerRoots.existingSecret=emilia-gate-issuer-roots \
40
+ --atomic --wait
41
+ ```
42
+
43
+ The pre-install/pre-upgrade migration Job must complete before Helm rolls the
44
+ Deployment. It is bounded by an active deadline and runs non-root with a
45
+ read-only root filesystem. The Deployment defaults to two replicas,
46
+ `maxUnavailable: 0`, a one-pod PDB, resource requests/limits, startup/liveness/
47
+ readiness probes, and a writable memory/scratch volume only at `/tmp`.
48
+ Rendering fails if migrations are enabled without a migration Secret, or if
49
+ that Secret name equals the runtime Postgres Secret. Helm cannot compare Secret
50
+ contents; provision different database login credentials in the two Secrets
51
+ and verify the runtime login has no DDL privileges.
52
+
53
+ ## Network policy
54
+
55
+ The default NetworkPolicy permits DNS, same-namespace ingress, and Postgres on
56
+ port 5432 only to a same-namespace pod labeled
57
+ `app.kubernetes.io/name=postgresql`. Managed Postgres requires an operator to
58
+ set `networkPolicy.egress.postgres.podSelector=null` plus stable
59
+ `networkPolicy.egress.postgres.cidrs` (or provide an additional CNI policy).
60
+
61
+ Standard Kubernetes NetworkPolicy does not support DNS names. GitHub API,
62
+ cloud KMS, and SIEM endpoints commonly use changing address ranges. Do not copy
63
+ today's public IP list into a long-lived manifest and assume it remains valid.
64
+ Prefer a controlled egress proxy with stable CIDRs or a CNI FQDN policy, then
65
+ set `kmsCidrs`, `githubCidrs`, and `siemCidrs` to the stable enforcement points.
66
+ Until configured, those outbound paths remain denied.
67
+
68
+ On a first Helm install, the migration is a pre-install hook and therefore runs
69
+ before this release's regular NetworkPolicy exists. Enforce a namespace-level
70
+ baseline egress policy before installation; subsequent upgrades are covered by
71
+ the existing release policy.
72
+
73
+ ## Clusterless check
74
+
75
+ ```bash
76
+ packages/gate/deploy/helm/emilia-gate-service/tests/render-check.sh
77
+ ```
78
+
79
+ The script runs strict Helm linting, renders with documentation-only registry
80
+ and Secret names, asserts the hardening resources and settings, and rejects any
81
+ rendered `Secret`. It does not connect to a cluster.
@@ -0,0 +1,12 @@
1
+ EMILIA Gate service was rendered as a BYOC deployment.
2
+
3
+ Image: {{ include "emilia-gate-service.image" . }}
4
+ Service: {{ include "emilia-gate-service.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }}
5
+
6
+ The chart did not create any Secret. Operator configuration, Postgres, KMS, and
7
+ issuer-root values are referenced from existing Secret names supplied at
8
+ install time.
9
+
10
+ Readiness:
11
+ kubectl -n {{ .Release.Namespace }} port-forward service/{{ include "emilia-gate-service.fullname" . }} 8080:{{ .Values.service.port }}
12
+ curl --fail http://127.0.0.1:8080{{ .Values.probes.readiness.path }}
@@ -0,0 +1,83 @@
1
+ {{- define "emilia-gate-service.name" -}}
2
+ {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
3
+ {{- end -}}
4
+
5
+ {{- define "emilia-gate-service.fullname" -}}
6
+ {{- if .Values.fullnameOverride -}}
7
+ {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
8
+ {{- else -}}
9
+ {{- printf "%s-%s" .Release.Name (include "emilia-gate-service.name" .) | trunc 63 | trimSuffix "-" -}}
10
+ {{- end -}}
11
+ {{- end -}}
12
+
13
+ {{- define "emilia-gate-service.labels" -}}
14
+ helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | quote }}
15
+ app.kubernetes.io/name: {{ include "emilia-gate-service.name" . }}
16
+ app.kubernetes.io/instance: {{ .Release.Name }}
17
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
18
+ app.kubernetes.io/part-of: emilia-gate
19
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
20
+ emiliaprotocol.ai/deployment-model: byoc
21
+ {{- end -}}
22
+
23
+ {{- define "emilia-gate-service.baseSelectorLabels" -}}
24
+ app.kubernetes.io/name: {{ include "emilia-gate-service.name" . }}
25
+ app.kubernetes.io/instance: {{ .Release.Name }}
26
+ {{- end -}}
27
+
28
+ {{- define "emilia-gate-service.serviceSelectorLabels" -}}
29
+ {{ include "emilia-gate-service.baseSelectorLabels" . }}
30
+ app.kubernetes.io/component: service
31
+ {{- end -}}
32
+
33
+ {{- define "emilia-gate-service.image" -}}
34
+ {{- $repository := required "image.repository is required; build and push Dockerfile.gate to a registry you control" .Values.image.repository -}}
35
+ {{- if and .Values.image.tag .Values.image.digest -}}
36
+ {{- fail "set exactly one of image.tag or image.digest" -}}
37
+ {{- else if .Values.image.digest -}}
38
+ {{- if not (regexMatch "^sha256:[a-f0-9]{64}$" .Values.image.digest) -}}
39
+ {{- fail "image.digest must be sha256:<64 lowercase hex characters>" -}}
40
+ {{- end -}}
41
+ {{- printf "%s@%s" $repository .Values.image.digest -}}
42
+ {{- else if .Values.image.tag -}}
43
+ {{- if eq (lower .Values.image.tag) "latest" -}}
44
+ {{- fail "image.tag=latest is mutable and refused; use an immutable version tag or digest" -}}
45
+ {{- end -}}
46
+ {{- printf "%s:%s" $repository .Values.image.tag -}}
47
+ {{- else -}}
48
+ {{- fail "set exactly one of image.tag or image.digest; mutable implicit latest is refused" -}}
49
+ {{- end -}}
50
+ {{- end -}}
51
+
52
+ {{- define "emilia-gate-service.postgresSecret" -}}
53
+ {{- required "secrets.postgres.existingSecret is required; the chart only references existing Secrets" .Values.secrets.postgres.existingSecret -}}
54
+ {{- end -}}
55
+
56
+ {{- define "emilia-gate-service.migrationPostgresSecret" -}}
57
+ {{- $runtimeSecret := include "emilia-gate-service.postgresSecret" . -}}
58
+ {{- $migrationSecret := required "migrations.postgres.existingSecret is required when migrations.enabled=true; migration credentials never fall back to the runtime Secret" .Values.migrations.postgres.existingSecret -}}
59
+ {{- if eq $migrationSecret $runtimeSecret -}}
60
+ {{- fail "migrations.postgres.existingSecret must name a Secret distinct from secrets.postgres.existingSecret" -}}
61
+ {{- end -}}
62
+ {{- $migrationSecret -}}
63
+ {{- end -}}
64
+
65
+ {{- define "emilia-gate-service.migrationPostgresKey" -}}
66
+ {{- required "migrations.postgres.key is required when migrations.enabled=true" .Values.migrations.postgres.key -}}
67
+ {{- end -}}
68
+
69
+ {{- define "emilia-gate-service.configurationSecret" -}}
70
+ {{- required "configuration.existingSecret is required; apps/gate-service requires an operator-owned gate.config.mjs" .Values.configuration.existingSecret -}}
71
+ {{- end -}}
72
+
73
+ {{- define "emilia-gate-service.apiTokenSecret" -}}
74
+ {{- required "secrets.apiToken.existingSecret is required; action routes require operator authentication" .Values.secrets.apiToken.existingSecret -}}
75
+ {{- end -}}
76
+
77
+ {{- define "emilia-gate-service.kmsSecret" -}}
78
+ {{- .Values.secrets.kms.existingSecret -}}
79
+ {{- end -}}
80
+
81
+ {{- define "emilia-gate-service.issuerRootsSecret" -}}
82
+ {{- required "secrets.issuerRoots.existingSecret is required; the chart only references existing Secrets" .Values.secrets.issuerRoots.existingSecret -}}
83
+ {{- end -}}
@@ -0,0 +1,153 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ include "emilia-gate-service.fullname" . }}
5
+ namespace: {{ .Release.Namespace }}
6
+ labels:
7
+ {{- include "emilia-gate-service.labels" . | nindent 4 }}
8
+ spec:
9
+ replicas: {{ .Values.replicaCount }}
10
+ revisionHistoryLimit: 3
11
+ strategy:
12
+ type: RollingUpdate
13
+ rollingUpdate:
14
+ maxUnavailable: 0
15
+ maxSurge: 1
16
+ selector:
17
+ matchLabels:
18
+ {{- include "emilia-gate-service.serviceSelectorLabels" . | nindent 6 }}
19
+ template:
20
+ metadata:
21
+ labels:
22
+ {{- include "emilia-gate-service.serviceSelectorLabels" . | nindent 8 }}
23
+ {{- with .Values.podLabels }}
24
+ {{- toYaml . | nindent 8 }}
25
+ {{- end }}
26
+ {{- with .Values.podAnnotations }}
27
+ annotations:
28
+ {{- toYaml . | nindent 8 }}
29
+ {{- end }}
30
+ spec:
31
+ serviceAccountName: {{ include "emilia-gate-service.fullname" . }}
32
+ automountServiceAccountToken: false
33
+ terminationGracePeriodSeconds: {{ .Values.runtime.terminationGracePeriodSeconds }}
34
+ securityContext:
35
+ {{- toYaml .Values.podSecurityContext | nindent 8 }}
36
+ {{- with .Values.imagePullSecrets }}
37
+ imagePullSecrets:
38
+ {{- toYaml . | nindent 8 }}
39
+ {{- end }}
40
+ containers:
41
+ - name: gate-service
42
+ image: {{ include "emilia-gate-service.image" . | quote }}
43
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
44
+ securityContext:
45
+ {{- toYaml .Values.containerSecurityContext | nindent 12 }}
46
+ ports:
47
+ - name: http
48
+ containerPort: {{ .Values.runtime.port }}
49
+ protocol: TCP
50
+ env:
51
+ - name: NODE_ENV
52
+ value: production
53
+ - name: HOST
54
+ value: 0.0.0.0
55
+ - name: PORT
56
+ value: {{ .Values.runtime.port | quote }}
57
+ - name: EMILIA_GATE_CONFIG
58
+ value: {{ printf "%s/%s" .Values.configuration.mountPath .Values.configuration.configKey | quote }}
59
+ - name: EP_GATE_PORT
60
+ value: {{ .Values.runtime.port | quote }}
61
+ - name: EP_GATE_LOG_LEVEL
62
+ value: {{ .Values.runtime.logLevel | quote }}
63
+ - name: {{ .Values.secrets.postgres.envName }}
64
+ valueFrom:
65
+ secretKeyRef:
66
+ name: {{ include "emilia-gate-service.postgresSecret" . }}
67
+ key: {{ .Values.secrets.postgres.key }}
68
+ optional: false
69
+ - name: {{ .Values.secrets.apiToken.envName }}
70
+ valueFrom:
71
+ secretKeyRef:
72
+ name: {{ include "emilia-gate-service.apiTokenSecret" . }}
73
+ key: {{ .Values.secrets.apiToken.key }}
74
+ optional: false
75
+ {{- if .Values.secrets.kms.existingSecret }}
76
+ - name: {{ .Values.secrets.kms.envName }}
77
+ valueFrom:
78
+ secretKeyRef:
79
+ name: {{ include "emilia-gate-service.kmsSecret" . }}
80
+ key: {{ .Values.secrets.kms.key }}
81
+ optional: false
82
+ {{- end }}
83
+ - name: {{ .Values.secrets.issuerRoots.envName }}
84
+ valueFrom:
85
+ secretKeyRef:
86
+ name: {{ include "emilia-gate-service.issuerRootsSecret" . }}
87
+ key: {{ .Values.secrets.issuerRoots.key }}
88
+ optional: false
89
+ {{- with .Values.runtime.extraEnv }}
90
+ {{- toYaml . | nindent 12 }}
91
+ {{- end }}
92
+ startupProbe:
93
+ httpGet:
94
+ path: {{ .Values.probes.startup.path }}
95
+ port: http
96
+ periodSeconds: {{ .Values.probes.startup.periodSeconds }}
97
+ timeoutSeconds: {{ .Values.probes.startup.timeoutSeconds }}
98
+ failureThreshold: {{ .Values.probes.startup.failureThreshold }}
99
+ livenessProbe:
100
+ httpGet:
101
+ path: {{ .Values.probes.liveness.path }}
102
+ port: http
103
+ periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
104
+ timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
105
+ failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
106
+ readinessProbe:
107
+ httpGet:
108
+ path: {{ .Values.probes.readiness.path }}
109
+ port: http
110
+ periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
111
+ timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
112
+ failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
113
+ resources:
114
+ {{- toYaml .Values.resources | nindent 12 }}
115
+ volumeMounts:
116
+ - name: tmp
117
+ mountPath: /tmp
118
+ - name: configuration
119
+ mountPath: {{ .Values.configuration.mountPath }}
120
+ readOnly: true
121
+ volumes:
122
+ - name: tmp
123
+ emptyDir:
124
+ sizeLimit: 64Mi
125
+ - name: configuration
126
+ secret:
127
+ secretName: {{ include "emilia-gate-service.configurationSecret" . }}
128
+ optional: false
129
+ defaultMode: 0440
130
+ items:
131
+ - key: {{ .Values.configuration.configKey }}
132
+ path: {{ .Values.configuration.configKey }}
133
+ topologySpreadConstraints:
134
+ {{- range .Values.topologySpreadConstraints }}
135
+ - maxSkew: {{ .maxSkew }}
136
+ topologyKey: {{ .topologyKey }}
137
+ whenUnsatisfiable: {{ .whenUnsatisfiable }}
138
+ labelSelector:
139
+ matchLabels:
140
+ {{- include "emilia-gate-service.serviceSelectorLabels" $ | nindent 14 }}
141
+ {{- end }}
142
+ {{- with .Values.nodeSelector }}
143
+ nodeSelector:
144
+ {{- toYaml . | nindent 8 }}
145
+ {{- end }}
146
+ {{- with .Values.tolerations }}
147
+ tolerations:
148
+ {{- toYaml . | nindent 8 }}
149
+ {{- end }}
150
+ {{- with .Values.affinity }}
151
+ affinity:
152
+ {{- toYaml . | nindent 8 }}
153
+ {{- end }}