@emilia-protocol/gate 0.9.0 → 0.9.1
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/action-control-manifest.js +9 -3
- package/breakglass.js +349 -0
- package/deploy/helm/README.md +110 -0
- package/deploy/helm/emilia-gate/Chart.yaml +39 -0
- package/deploy/helm/emilia-gate/templates/NOTES.txt +48 -0
- package/deploy/helm/emilia-gate/templates/configmap.yaml +27 -0
- package/deploy/helm/emilia-gate/templates/deployment.yaml +134 -0
- package/deploy/helm/emilia-gate/templates/service.yaml +28 -0
- package/deploy/helm/emilia-gate/templates/servicemonitor.yaml +41 -0
- package/deploy/helm/emilia-gate/values.yaml +169 -0
- package/deploy/terraform/README.md +138 -0
- package/deploy/terraform/main.tf +256 -0
- package/deploy/terraform/outputs.tf +33 -0
- package/deploy/terraform/variables.tf +205 -0
- package/deploy/terraform/versions.tf +18 -0
- package/enterprise.js +174 -0
- package/index.js +125 -10
- package/metering.js +227 -0
- package/metrics.js +232 -0
- package/package.json +39 -4
- package/reliance-packet.js +80 -2
- package/reports/art14.js +0 -0
- package/reports/auditor-workpaper.js +538 -0
- package/reports/reperform.js +365 -0
- package/reports/underwriter.js +340 -0
- package/roster.js +265 -0
- package/siem.js +236 -0
- package/store-postgres.js +129 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# emilia-gate reference chart (EP-GATE-HELM-v1) — Deployment.
|
|
3
|
+
# Single-container gate service. Env-driven config; issuer keys come from an
|
|
4
|
+
# EXISTING Secret (never rendered here); the action-risk manifest is mounted
|
|
5
|
+
# read-only from a ConfigMap. Fails closed: rendering aborts without a
|
|
6
|
+
# pinned-issuer Secret reference.
|
|
7
|
+
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
8
|
+
{{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
|
|
9
|
+
{{- $manifestConfigMap := default $fullname .Values.manifest.existingConfigMap }}
|
|
10
|
+
apiVersion: apps/v1
|
|
11
|
+
kind: Deployment
|
|
12
|
+
metadata:
|
|
13
|
+
name: {{ $fullname }}
|
|
14
|
+
namespace: {{ .Release.Namespace }}
|
|
15
|
+
labels:
|
|
16
|
+
app.kubernetes.io/name: {{ $name }}
|
|
17
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
18
|
+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
19
|
+
app.kubernetes.io/component: trusted-action-firewall
|
|
20
|
+
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
21
|
+
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
|
|
22
|
+
emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
|
|
23
|
+
emiliaprotocol.ai/maturity: experimental
|
|
24
|
+
spec:
|
|
25
|
+
replicas: {{ .Values.replicaCount }}
|
|
26
|
+
selector:
|
|
27
|
+
matchLabels:
|
|
28
|
+
app.kubernetes.io/name: {{ $name }}
|
|
29
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
30
|
+
template:
|
|
31
|
+
metadata:
|
|
32
|
+
labels:
|
|
33
|
+
app.kubernetes.io/name: {{ $name }}
|
|
34
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
35
|
+
app.kubernetes.io/component: trusted-action-firewall
|
|
36
|
+
{{- with .Values.podLabels }}
|
|
37
|
+
{{- toYaml . | nindent 8 }}
|
|
38
|
+
{{- end }}
|
|
39
|
+
annotations:
|
|
40
|
+
{{- if not .Values.manifest.existingConfigMap }}
|
|
41
|
+
# Roll pods when the rendered action-risk manifest changes.
|
|
42
|
+
checksum/manifest: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
|
43
|
+
{{- end }}
|
|
44
|
+
{{- with .Values.podAnnotations }}
|
|
45
|
+
{{- toYaml . | nindent 8 }}
|
|
46
|
+
{{- end }}
|
|
47
|
+
spec:
|
|
48
|
+
{{- with .Values.imagePullSecrets }}
|
|
49
|
+
imagePullSecrets:
|
|
50
|
+
{{- toYaml . | nindent 8 }}
|
|
51
|
+
{{- end }}
|
|
52
|
+
# The gate never talks to the Kubernetes API — do not mount a token.
|
|
53
|
+
automountServiceAccountToken: false
|
|
54
|
+
securityContext:
|
|
55
|
+
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
56
|
+
containers:
|
|
57
|
+
- name: gate
|
|
58
|
+
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
|
|
59
|
+
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
60
|
+
securityContext:
|
|
61
|
+
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
|
62
|
+
ports:
|
|
63
|
+
- name: http
|
|
64
|
+
containerPort: {{ .Values.gate.port }}
|
|
65
|
+
protocol: TCP
|
|
66
|
+
env:
|
|
67
|
+
- name: NODE_ENV
|
|
68
|
+
value: production
|
|
69
|
+
- name: EP_GATE_PORT
|
|
70
|
+
value: {{ .Values.gate.port | quote }}
|
|
71
|
+
- name: EP_GATE_LOG_LEVEL
|
|
72
|
+
value: {{ .Values.gate.logLevel | quote }}
|
|
73
|
+
# Strict evidence log: never authorize an action the gate cannot
|
|
74
|
+
# durably account for.
|
|
75
|
+
- name: EP_GATE_EVIDENCE_STRICT
|
|
76
|
+
value: {{ .Values.gate.evidenceStrict | quote }}
|
|
77
|
+
- name: EP_GATE_MANIFEST_PATH
|
|
78
|
+
value: /etc/emilia-gate/{{ .Values.manifest.fileName }}
|
|
79
|
+
- name: EP_GATE_METRICS_ENABLED
|
|
80
|
+
value: {{ .Values.metrics.enabled | quote }}
|
|
81
|
+
# Pinned issuer keys — REFERENCED from an existing Secret, never
|
|
82
|
+
# rendered into the chart. `required` makes helm refuse to render
|
|
83
|
+
# a gate with no pinned issuers: fail closed.
|
|
84
|
+
- name: EP_GATE_ISSUER_KEYS
|
|
85
|
+
valueFrom:
|
|
86
|
+
secretKeyRef:
|
|
87
|
+
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
|
+
key: {{ .Values.issuerKeys.secretKey }}
|
|
89
|
+
{{- with .Values.gate.extraEnv }}
|
|
90
|
+
{{- toYaml . | nindent 12 }}
|
|
91
|
+
{{- end }}
|
|
92
|
+
livenessProbe:
|
|
93
|
+
httpGet:
|
|
94
|
+
path: {{ .Values.probes.liveness.path }}
|
|
95
|
+
port: http
|
|
96
|
+
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
|
|
97
|
+
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
|
|
98
|
+
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
|
|
99
|
+
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
|
|
100
|
+
readinessProbe:
|
|
101
|
+
httpGet:
|
|
102
|
+
path: {{ .Values.probes.readiness.path }}
|
|
103
|
+
port: http
|
|
104
|
+
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
|
|
105
|
+
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
|
|
106
|
+
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
|
|
107
|
+
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
|
|
108
|
+
resources:
|
|
109
|
+
{{- toYaml .Values.resources | nindent 12 }}
|
|
110
|
+
volumeMounts:
|
|
111
|
+
- name: action-risk-manifest
|
|
112
|
+
mountPath: /etc/emilia-gate
|
|
113
|
+
readOnly: true
|
|
114
|
+
# Writable scratch for the runtime; rootfs stays read-only.
|
|
115
|
+
- name: tmp
|
|
116
|
+
mountPath: /tmp
|
|
117
|
+
volumes:
|
|
118
|
+
- name: action-risk-manifest
|
|
119
|
+
configMap:
|
|
120
|
+
name: {{ $manifestConfigMap }}
|
|
121
|
+
- name: tmp
|
|
122
|
+
emptyDir: {}
|
|
123
|
+
{{- with .Values.nodeSelector }}
|
|
124
|
+
nodeSelector:
|
|
125
|
+
{{- toYaml . | nindent 8 }}
|
|
126
|
+
{{- end }}
|
|
127
|
+
{{- with .Values.tolerations }}
|
|
128
|
+
tolerations:
|
|
129
|
+
{{- toYaml . | nindent 8 }}
|
|
130
|
+
{{- end }}
|
|
131
|
+
{{- with .Values.affinity }}
|
|
132
|
+
affinity:
|
|
133
|
+
{{- toYaml . | nindent 8 }}
|
|
134
|
+
{{- end }}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# emilia-gate reference chart (EP-GATE-HELM-v1) — Service.
|
|
3
|
+
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
4
|
+
{{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
|
|
5
|
+
apiVersion: v1
|
|
6
|
+
kind: Service
|
|
7
|
+
metadata:
|
|
8
|
+
name: {{ $fullname }}
|
|
9
|
+
namespace: {{ .Release.Namespace }}
|
|
10
|
+
labels:
|
|
11
|
+
app.kubernetes.io/name: {{ $name }}
|
|
12
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
13
|
+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
14
|
+
app.kubernetes.io/component: trusted-action-firewall
|
|
15
|
+
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
16
|
+
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
|
|
17
|
+
emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
|
|
18
|
+
emiliaprotocol.ai/maturity: experimental
|
|
19
|
+
spec:
|
|
20
|
+
type: {{ .Values.service.type }}
|
|
21
|
+
ports:
|
|
22
|
+
- name: http
|
|
23
|
+
port: {{ .Values.service.port }}
|
|
24
|
+
targetPort: http
|
|
25
|
+
protocol: TCP
|
|
26
|
+
selector:
|
|
27
|
+
app.kubernetes.io/name: {{ $name }}
|
|
28
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# emilia-gate reference chart (EP-GATE-HELM-v1) — optional ServiceMonitor STUB.
|
|
3
|
+
# Gated behind metrics.enabled; scrapes the gate's /metrics endpoint. Requires
|
|
4
|
+
# the Prometheus Operator CRDs (monitoring.coreos.com/v1) to be installed —
|
|
5
|
+
# installing this chart with metrics.enabled=true on a cluster without them
|
|
6
|
+
# will fail at apply time. Stub: adapt relabelings/TLS to your monitoring
|
|
7
|
+
# stack.
|
|
8
|
+
{{- if .Values.metrics.enabled }}
|
|
9
|
+
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
10
|
+
{{- $fullname := default (printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-") .Values.fullnameOverride }}
|
|
11
|
+
apiVersion: monitoring.coreos.com/v1
|
|
12
|
+
kind: ServiceMonitor
|
|
13
|
+
metadata:
|
|
14
|
+
name: {{ $fullname }}
|
|
15
|
+
namespace: {{ .Release.Namespace }}
|
|
16
|
+
labels:
|
|
17
|
+
app.kubernetes.io/name: {{ $name }}
|
|
18
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
19
|
+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
20
|
+
app.kubernetes.io/component: trusted-action-firewall
|
|
21
|
+
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
22
|
+
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
|
|
23
|
+
emiliaprotocol.ai/chart-contract: EP-GATE-HELM-v1
|
|
24
|
+
emiliaprotocol.ai/maturity: experimental
|
|
25
|
+
{{- with .Values.metrics.serviceMonitor.additionalLabels }}
|
|
26
|
+
{{- toYaml . | nindent 4 }}
|
|
27
|
+
{{- end }}
|
|
28
|
+
spec:
|
|
29
|
+
selector:
|
|
30
|
+
matchLabels:
|
|
31
|
+
app.kubernetes.io/name: {{ $name }}
|
|
32
|
+
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
33
|
+
namespaceSelector:
|
|
34
|
+
matchNames:
|
|
35
|
+
- {{ .Release.Namespace }}
|
|
36
|
+
endpoints:
|
|
37
|
+
- port: http
|
|
38
|
+
path: {{ .Values.metrics.path }}
|
|
39
|
+
interval: {{ .Values.metrics.serviceMonitor.interval }}
|
|
40
|
+
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
|
|
41
|
+
{{- end }}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Default values for the emilia-gate reference chart (EP-GATE-HELM-v1).
|
|
3
|
+
#
|
|
4
|
+
# HONEST LABEL: reference chart, experimental, BYOC. Review before use.
|
|
5
|
+
#
|
|
6
|
+
# SECURITY INVARIANTS (fail-closed by design):
|
|
7
|
+
# 1. Issuer key material is NEVER rendered by this chart. You MUST create a
|
|
8
|
+
# Kubernetes Secret out-of-band and point issuerKeys.existingSecret at
|
|
9
|
+
# it — templating fails (helm refuses to render) if it is unset.
|
|
10
|
+
# 2. The evidence log defaults to strict mode: the gate refuses to authorize
|
|
11
|
+
# an action it cannot durably account for.
|
|
12
|
+
# 3. Pod security defaults are hardened (non-root, read-only rootfs, no
|
|
13
|
+
# capabilities, no service-account token). Loosen deliberately, not
|
|
14
|
+
# accidentally.
|
|
15
|
+
|
|
16
|
+
replicaCount: 2
|
|
17
|
+
|
|
18
|
+
image:
|
|
19
|
+
# BYOC: build and push your own image of the gate service; there is no
|
|
20
|
+
# official public image while the chart is experimental.
|
|
21
|
+
repository: ghcr.io/emilia-protocol/gate
|
|
22
|
+
# Defaults to the chart's appVersion when empty.
|
|
23
|
+
tag: ""
|
|
24
|
+
pullPolicy: IfNotPresent
|
|
25
|
+
|
|
26
|
+
imagePullSecrets: []
|
|
27
|
+
nameOverride: ""
|
|
28
|
+
fullnameOverride: ""
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Gate service configuration — everything is env-driven; the container reads
|
|
32
|
+
# these EP_GATE_* variables at boot (see README.md for the full contract).
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
gate:
|
|
35
|
+
# Port the gate HTTP service listens on (EP_GATE_PORT).
|
|
36
|
+
port: 8080
|
|
37
|
+
# Log verbosity (EP_GATE_LOG_LEVEL): error | warn | info | debug
|
|
38
|
+
logLevel: info
|
|
39
|
+
# Strict evidence log (EP_GATE_EVIDENCE_STRICT): when "true" the gate fails
|
|
40
|
+
# CLOSED if a decision record cannot be durably written. Keep true.
|
|
41
|
+
evidenceStrict: true
|
|
42
|
+
# Extra environment variables appended verbatim to the container (list of
|
|
43
|
+
# EnvVar objects — supports valueFrom for your own Secret/ConfigMap refs).
|
|
44
|
+
extraEnv: []
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Issuer keys — REQUIRED, referenced, never rendered.
|
|
48
|
+
# Create the Secret yourself, e.g.:
|
|
49
|
+
# kubectl create secret generic emilia-gate-issuer-keys \
|
|
50
|
+
# --from-file=issuer-keys.json=./issuer-keys.json
|
|
51
|
+
# where issuer-keys.json is a JSON array of pinned base64url SPKI-DER Ed25519
|
|
52
|
+
# public keys (or {kid: key} map). The chart exposes it to the container as
|
|
53
|
+
# EP_GATE_ISSUER_KEYS via a secretKeyRef. Rendering fails if existingSecret
|
|
54
|
+
# is empty — a gate with no pinned issuers must not deploy (fail closed).
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
issuerKeys:
|
|
57
|
+
existingSecret: ""
|
|
58
|
+
secretKey: "issuer-keys.json"
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Action-risk manifest (EP-ACTION-RISK-MANIFEST-v0.1) — which actions are
|
|
62
|
+
# guarded and what assurance each one requires. Rendered into a ConfigMap and
|
|
63
|
+
# mounted read-only; the container reads it from EP_GATE_MANIFEST_PATH.
|
|
64
|
+
# Set existingConfigMap to bring your own instead of the inline default.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
manifest:
|
|
67
|
+
# File name of the manifest key inside the ConfigMap / mount.
|
|
68
|
+
fileName: action-risk-manifest.json
|
|
69
|
+
# Name of a pre-existing ConfigMap carrying the manifest under `fileName`.
|
|
70
|
+
# When set, the chart's own ConfigMap is not rendered.
|
|
71
|
+
existingConfigMap: ""
|
|
72
|
+
# Inline manifest — rendered to JSON verbatim. The default below is a
|
|
73
|
+
# SUBSET of the gate's built-in high-risk action packs (see
|
|
74
|
+
# packages/gate/action-packs.js for the full set); replace it with your
|
|
75
|
+
# organization's real manifest.
|
|
76
|
+
inline:
|
|
77
|
+
"@version": EP-ACTION-RISK-MANIFEST-v0.1
|
|
78
|
+
actions:
|
|
79
|
+
- id: money_movement.release
|
|
80
|
+
label: Money movement
|
|
81
|
+
action_type: payment.release
|
|
82
|
+
risk: critical
|
|
83
|
+
receipt_required: true
|
|
84
|
+
assurance_class: class_a
|
|
85
|
+
match: { protocol: mcp, tool: release_payment }
|
|
86
|
+
execution_binding:
|
|
87
|
+
required_fields:
|
|
88
|
+
- action_type
|
|
89
|
+
- amount_usd
|
|
90
|
+
- currency
|
|
91
|
+
- payment_instruction_id
|
|
92
|
+
- beneficiary_account_hash
|
|
93
|
+
- id: production.deploy
|
|
94
|
+
label: Production deploy
|
|
95
|
+
action_type: deploy.production
|
|
96
|
+
risk: critical
|
|
97
|
+
receipt_required: true
|
|
98
|
+
assurance_class: quorum
|
|
99
|
+
match: { protocol: mcp, tool: deploy_production }
|
|
100
|
+
execution_binding:
|
|
101
|
+
required_fields:
|
|
102
|
+
- action_type
|
|
103
|
+
- repo
|
|
104
|
+
- commit_sha
|
|
105
|
+
- environment
|
|
106
|
+
- artifact_digest
|
|
107
|
+
|
|
108
|
+
service:
|
|
109
|
+
type: ClusterIP
|
|
110
|
+
port: 8080
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Metrics — matches the gate's /metrics endpoint. When enabled, the chart
|
|
114
|
+
# sets EP_GATE_METRICS_ENABLED=true and renders a ServiceMonitor stub
|
|
115
|
+
# (requires the Prometheus Operator CRDs to be installed in the cluster).
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
metrics:
|
|
118
|
+
enabled: false
|
|
119
|
+
path: /metrics
|
|
120
|
+
serviceMonitor:
|
|
121
|
+
interval: 30s
|
|
122
|
+
scrapeTimeout: 10s
|
|
123
|
+
# Extra labels for the ServiceMonitor (e.g. `release: prometheus` so your
|
|
124
|
+
# Prometheus instance selects it).
|
|
125
|
+
additionalLabels: {}
|
|
126
|
+
|
|
127
|
+
probes:
|
|
128
|
+
liveness:
|
|
129
|
+
path: /healthz
|
|
130
|
+
initialDelaySeconds: 5
|
|
131
|
+
periodSeconds: 10
|
|
132
|
+
timeoutSeconds: 2
|
|
133
|
+
failureThreshold: 3
|
|
134
|
+
readiness:
|
|
135
|
+
path: /readyz
|
|
136
|
+
initialDelaySeconds: 5
|
|
137
|
+
periodSeconds: 10
|
|
138
|
+
timeoutSeconds: 2
|
|
139
|
+
failureThreshold: 3
|
|
140
|
+
|
|
141
|
+
resources:
|
|
142
|
+
requests:
|
|
143
|
+
cpu: 100m
|
|
144
|
+
memory: 128Mi
|
|
145
|
+
limits:
|
|
146
|
+
cpu: 500m
|
|
147
|
+
memory: 256Mi
|
|
148
|
+
|
|
149
|
+
# Hardened pod defaults — loosen deliberately, not accidentally.
|
|
150
|
+
podSecurityContext:
|
|
151
|
+
runAsNonRoot: true
|
|
152
|
+
runAsUser: 10001
|
|
153
|
+
runAsGroup: 10001
|
|
154
|
+
fsGroup: 10001
|
|
155
|
+
seccompProfile:
|
|
156
|
+
type: RuntimeDefault
|
|
157
|
+
|
|
158
|
+
containerSecurityContext:
|
|
159
|
+
allowPrivilegeEscalation: false
|
|
160
|
+
readOnlyRootFilesystem: true
|
|
161
|
+
capabilities:
|
|
162
|
+
drop:
|
|
163
|
+
- ALL
|
|
164
|
+
|
|
165
|
+
podAnnotations: {}
|
|
166
|
+
podLabels: {}
|
|
167
|
+
nodeSelector: {}
|
|
168
|
+
tolerations: []
|
|
169
|
+
affinity: {}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# EMILIA Gate — reference Terraform module (`EP-GATE-TF-v1`)
|
|
4
|
+
|
|
5
|
+
Terraform sibling of the Helm chart (`../helm`, `EP-GATE-HELM-v1`) for BYOC
|
|
6
|
+
(bring-your-own-cloud) installs of the Trusted Action Firewall. Same container
|
|
7
|
+
contract, same fail-closed invariants, expressed as three Kubernetes resources
|
|
8
|
+
in a cluster **you** control:
|
|
9
|
+
|
|
10
|
+
| Resource | Purpose |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `kubernetes_config_map_v1.manifest` | The action-risk manifest (the deny-by-default policy), mounted read-only at `/etc/emilia-gate/action-risk-manifest.json` |
|
|
13
|
+
| `kubernetes_deployment_v1.gate` | The gate pods — non-root, read-only rootfs, no privilege escalation, all capabilities dropped, no service-account token; pinned issuer keys arrive via a non-optional `secretKeyRef` to **your** Secret |
|
|
14
|
+
| `kubernetes_service_v1.gate` | Cluster-internal Service (ClusterIP by default) in front of the pods |
|
|
15
|
+
|
|
16
|
+
**Honest framing:** this is a *reference module* and it is *experimental*. It
|
|
17
|
+
is a starting point for your platform team to review, fork, and own — not a
|
|
18
|
+
managed service, not a turnkey production install. The deployer's cluster, the
|
|
19
|
+
deployer's keys, the deployer's responsibility for network policy, ingress,
|
|
20
|
+
TLS, and durable storage. There is no official public gate image while this is
|
|
21
|
+
experimental: build and push your own.
|
|
22
|
+
|
|
23
|
+
## Key custody — read this first
|
|
24
|
+
|
|
25
|
+
Issuer public keys (the keys the gate pins and trusts to have signed receipts)
|
|
26
|
+
are consumed from an **existing Kubernetes Secret that you create**. The module
|
|
27
|
+
takes the Secret's *name* only:
|
|
28
|
+
|
|
29
|
+
- it never accepts key material inline,
|
|
30
|
+
- it never creates or reads a Secret, so no key bytes enter Terraform state,
|
|
31
|
+
- the `secretKeyRef` is `optional = false` — **no pinned issuers, no pods**. A
|
|
32
|
+
gate without pinned issuer keys must never come up permissive, so it fails
|
|
33
|
+
closed by not coming up at all.
|
|
34
|
+
|
|
35
|
+
Create the Secret out-of-band (or via your own secrets tooling — ESO, sealed
|
|
36
|
+
secrets, Vault injector):
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
kubectl -n emilia create secret generic emilia-gate-issuer-keys \
|
|
40
|
+
--from-file=issuer-keys.json=./issuer-keys.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
where `issuer-keys.json` is a JSON array of pinned base64url SPKI-DER Ed25519
|
|
44
|
+
public keys (or a `{kid: key}` map). It reaches the container as
|
|
45
|
+
`EP_GATE_ISSUER_KEYS`.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```hcl
|
|
50
|
+
provider "kubernetes" {
|
|
51
|
+
config_path = "~/.kube/config" # your cluster, your credentials
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module "emilia_gate" {
|
|
55
|
+
source = "github.com/FutureEnterprises/emilia-protocol//packages/gate/deploy/terraform"
|
|
56
|
+
|
|
57
|
+
namespace = "emilia" # must already exist
|
|
58
|
+
image = "ghcr.io/your-org/emilia-gate@sha256:..." # pin a digest
|
|
59
|
+
|
|
60
|
+
replicas = 2
|
|
61
|
+
issuer_keys_secret_name = "emilia-gate-issuer-keys" # NAME only, never keys
|
|
62
|
+
|
|
63
|
+
# The deny-by-default policy the gate enforces (EP-ACTION-RISK-MANIFEST).
|
|
64
|
+
manifest_json = file("${path.module}/manifest.json")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
output "gate_endpoint" {
|
|
68
|
+
value = module.emilia_gate.service_endpoint
|
|
69
|
+
# e.g. http://emilia-gate.emilia.svc.cluster.local:8080
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The manifest is validated at `terraform plan` time (must be JSON with an
|
|
74
|
+
`actions` field), and its sha256 is annotated onto the pod template so every
|
|
75
|
+
policy change rolls the pods.
|
|
76
|
+
|
|
77
|
+
## Container contract (identical to the Helm chart)
|
|
78
|
+
|
|
79
|
+
| Env var | Source | Default |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `NODE_ENV` | fixed | `production` |
|
|
82
|
+
| `EP_GATE_PORT` | `port` | `8080` |
|
|
83
|
+
| `EP_GATE_LOG_LEVEL` | `log_level` | `info` |
|
|
84
|
+
| `EP_GATE_EVIDENCE_STRICT` | `evidence_strict` | `true` |
|
|
85
|
+
| `EP_GATE_MANIFEST_PATH` | ConfigMap mount | `/etc/emilia-gate/action-risk-manifest.json` |
|
|
86
|
+
| `EP_GATE_METRICS_ENABLED` | `metrics_enabled` | `false` |
|
|
87
|
+
| `EP_GATE_ISSUER_KEYS` | `secretKeyRef` → your existing Secret | — (required) |
|
|
88
|
+
|
|
89
|
+
Extra plain-text env goes in `extra_env` — **never secrets** (values in that
|
|
90
|
+
map land in Terraform state); wire secret env from your own Secrets.
|
|
91
|
+
|
|
92
|
+
## Inputs
|
|
93
|
+
|
|
94
|
+
| Name | Default | Notes |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `image` | — (required) | Pin a digest or exact tag |
|
|
97
|
+
| `manifest_json` | — (required) | JSON string; plan-time validated |
|
|
98
|
+
| `issuer_keys_secret_name` | — (required) | Existing Secret, same namespace |
|
|
99
|
+
| `issuer_keys_secret_key` | `issuer-keys.json` | Key inside that Secret |
|
|
100
|
+
| `name` | `emilia-gate` | DNS-1123 label |
|
|
101
|
+
| `namespace` | `default` | Must already exist |
|
|
102
|
+
| `replicas` | `2` | See replay-defense note below |
|
|
103
|
+
| `port` / `service_port` | `8080` / `8080` | |
|
|
104
|
+
| `service_type` | `ClusterIP` | Anything more exposed is your explicit call |
|
|
105
|
+
| `log_level` | `info` | `error` \| `warn` \| `info` \| `debug` |
|
|
106
|
+
| `evidence_strict` | `true` | Keep true — fail-closed evidence log |
|
|
107
|
+
| `metrics_enabled` | `false` | Scrape wiring is chart-only for now |
|
|
108
|
+
| `liveness_path` / `readiness_path` | `/healthz` / `/readyz` | `null` disables |
|
|
109
|
+
| `resources` | 100m/128Mi → 500m/256Mi | |
|
|
110
|
+
| `run_as_user` | `10001` | Non-root enforced |
|
|
111
|
+
| `read_only_root_filesystem` | `true` | `/tmp` is a writable emptyDir either way |
|
|
112
|
+
| `extra_env`, `extra_labels` | `{}` | Plain-text only |
|
|
113
|
+
|
|
114
|
+
Outputs: `service_name`, `namespace`, `service_endpoint`, `deployment_name`,
|
|
115
|
+
`manifest_config_map_name`, `module_version`.
|
|
116
|
+
|
|
117
|
+
## Operational notes
|
|
118
|
+
|
|
119
|
+
- **Replay defense across replicas:** with `replicas > 1`, one-time receipt
|
|
120
|
+
consumption is only fleet-safe if the gate is configured with a *shared*
|
|
121
|
+
durable consumption store (Redis/Postgres — see
|
|
122
|
+
`createDurableConsumptionStore` in `@emilia-protocol/gate`). The module
|
|
123
|
+
deploys the pods either way; pointing them at a shared store (e.g. via
|
|
124
|
+
`extra_env`) is your configuration responsibility.
|
|
125
|
+
- **Namespace:** not created by this module — pass one that exists.
|
|
126
|
+
- **Ingress/TLS/NetworkPolicy/ServiceMonitor:** intentionally out of scope.
|
|
127
|
+
The default posture is cluster-internal; exposure decisions belong to the
|
|
128
|
+
deployer.
|
|
129
|
+
|
|
130
|
+
## What has been validated
|
|
131
|
+
|
|
132
|
+
`terraform fmt -check`, `terraform init -backend=false`, and
|
|
133
|
+
`terraform validate` pass against Terraform 1.9.8 with
|
|
134
|
+
`hashicorp/kubernetes` 2.38. A create-only `terraform plan` renders all three
|
|
135
|
+
resources, and the plan-time input validations (unparseable manifest, inline
|
|
136
|
+
key material, zero replicas) refuse as designed. The module has **not** been
|
|
137
|
+
applied against a live cluster as part of this repo's CI — treat it as
|
|
138
|
+
reviewed reference code, not a certified install path.
|