lcp-reference-platform 1.0.0__tar.gz
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.
- lcp_reference_platform-1.0.0/.gitignore +16 -0
- lcp_reference_platform-1.0.0/Dockerfile +33 -0
- lcp_reference_platform-1.0.0/PKG-INFO +22 -0
- lcp_reference_platform-1.0.0/README.md +248 -0
- lcp_reference_platform-1.0.0/docker-compose.yml +34 -0
- lcp_reference_platform-1.0.0/kubernetes/README.md +94 -0
- lcp_reference_platform-1.0.0/kubernetes/configmap.yaml +21 -0
- lcp_reference_platform-1.0.0/kubernetes/deployment.yaml +161 -0
- lcp_reference_platform-1.0.0/kubernetes/hpa.example.yaml +43 -0
- lcp_reference_platform-1.0.0/kubernetes/network-policy.example.yaml +80 -0
- lcp_reference_platform-1.0.0/kubernetes/pod-disruption-budget.yaml +19 -0
- lcp_reference_platform-1.0.0/kubernetes/pvc.example.yaml +11 -0
- lcp_reference_platform-1.0.0/kubernetes/secret.example.yaml +12 -0
- lcp_reference_platform-1.0.0/kubernetes/service.yaml +13 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/README.md +42 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/kyverno-test.yaml +32 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/provenance-policy.yaml +33 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/resources.yaml +35 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/sbom-policy.yaml +33 -0
- lcp_reference_platform-1.0.0/kubernetes/tests/verify-images/signature-policy.yaml +31 -0
- lcp_reference_platform-1.0.0/kubernetes/verify-images-kyverno.example.yaml +91 -0
- lcp_reference_platform-1.0.0/lcp_platform/__init__.py +17 -0
- lcp_reference_platform-1.0.0/lcp_platform/admin.py +90 -0
- lcp_reference_platform-1.0.0/lcp_platform/api_keys.py +50 -0
- lcp_reference_platform-1.0.0/lcp_platform/attachments.py +361 -0
- lcp_reference_platform-1.0.0/lcp_platform/auth.py +202 -0
- lcp_reference_platform-1.0.0/lcp_platform/config.py +157 -0
- lcp_reference_platform-1.0.0/lcp_platform/crypto.py +94 -0
- lcp_reference_platform-1.0.0/lcp_platform/database.py +18 -0
- lcp_reference_platform-1.0.0/lcp_platform/mapping.py +309 -0
- lcp_reference_platform-1.0.0/lcp_platform/matching.py +147 -0
- lcp_reference_platform-1.0.0/lcp_platform/messages.py +265 -0
- lcp_reference_platform-1.0.0/lcp_platform/postgres.py +235 -0
- lcp_reference_platform-1.0.0/lcp_platform/router.py +1116 -0
- lcp_reference_platform-1.0.0/lcp_platform/secrets.py +70 -0
- lcp_reference_platform-1.0.0/lcp_platform/security.py +76 -0
- lcp_reference_platform-1.0.0/lcp_platform/server.py +97 -0
- lcp_reference_platform-1.0.0/lcp_platform/service.py +147 -0
- lcp_reference_platform-1.0.0/lcp_platform/storage.py +1300 -0
- lcp_reference_platform-1.0.0/lcp_platform/validation.py +118 -0
- lcp_reference_platform-1.0.0/lcp_platform/worker.py +30 -0
- lcp_reference_platform-1.0.0/lcp_platform/wsgi.py +50 -0
- lcp_reference_platform-1.0.0/pyproject.toml +33 -0
- lcp_reference_platform-1.0.0/tests/test_api_keys.py +142 -0
- lcp_reference_platform-1.0.0/tests/test_new_flows.py +387 -0
- lcp_reference_platform-1.0.0/tests/test_platform.py +409 -0
- lcp_reference_platform-1.0.0/tests/test_postgres_integration.py +140 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
FROM python:3.12-alpine@sha256:78098ea6a3a9c6a7727a5d4674e4a44e57e01fac878ee9cb4d24a86bd93916ff
|
|
2
|
+
|
|
3
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
4
|
+
PYTHONUNBUFFERED=1 \
|
|
5
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
6
|
+
PIP_NO_CACHE_DIR=1 \
|
|
7
|
+
LCP_SCHEMA_DIR=/app/schemas \
|
|
8
|
+
LCP_DATABASE_PATH=/app/data/lcp.sqlite3 \
|
|
9
|
+
LCP_HOST=0.0.0.0 \
|
|
10
|
+
LCP_PORT=8080
|
|
11
|
+
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
|
|
14
|
+
COPY schemas /app/schemas
|
|
15
|
+
COPY verticals /app/verticals
|
|
16
|
+
COPY examples/sandbox /app/examples/sandbox
|
|
17
|
+
COPY implementations/reference-platform /app/implementations/reference-platform
|
|
18
|
+
|
|
19
|
+
RUN python -m pip install --no-cache-dir --upgrade pip 'setuptools>=78.1.1' \
|
|
20
|
+
&& python -m pip install --no-cache-dir -e '/app/implementations/reference-platform[production]' \
|
|
21
|
+
&& adduser -D -u 10001 -h /home/lcp -s /sbin/nologin lcp \
|
|
22
|
+
&& mkdir -p /app/data \
|
|
23
|
+
&& chown -R lcp:lcp /app/data /app/implementations
|
|
24
|
+
|
|
25
|
+
# The image has no compiler toolchain or application build dependencies. The
|
|
26
|
+
# Kubernetes example additionally enforces read-only rootfs, no privilege
|
|
27
|
+
# escalation, and zero Linux capabilities.
|
|
28
|
+
USER lcp
|
|
29
|
+
|
|
30
|
+
EXPOSE 8080
|
|
31
|
+
STOPSIGNAL SIGTERM
|
|
32
|
+
|
|
33
|
+
CMD ["lcp-platform"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lcp-reference-platform
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Production-oriented reference platform and router for LCP Lead Context Protocol deployments.
|
|
5
|
+
Project-URL: Documentation, https://github.com/SpearSystems/LCP
|
|
6
|
+
Project-URL: Source, https://github.com/SpearSystems/LCP
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: cryptography>=42.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
12
|
+
Requires-Dist: referencing>=0.35.0
|
|
13
|
+
Provides-Extra: object-storage
|
|
14
|
+
Requires-Dist: boto3>=1.35.0; extra == 'object-storage'
|
|
15
|
+
Requires-Dist: clamd>=1.0.2; extra == 'object-storage'
|
|
16
|
+
Provides-Extra: postgres
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'postgres'
|
|
18
|
+
Provides-Extra: production
|
|
19
|
+
Requires-Dist: boto3>=1.35.0; extra == 'production'
|
|
20
|
+
Requires-Dist: clamd>=1.0.2; extra == 'production'
|
|
21
|
+
Requires-Dist: gunicorn>=23.0; extra == 'production'
|
|
22
|
+
Requires-Dist: psycopg[binary]>=3.2; extra == 'production'
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# LCP Reference Platform
|
|
2
|
+
|
|
3
|
+
Production-oriented reference HTTP platform and router for the Lead Context
|
|
4
|
+
Protocol. It accepts publisher leads, applies buyer offers, runs the
|
|
5
|
+
ping/bid/post flow, persists state, and delivers signed webhooks with retries.
|
|
6
|
+
|
|
7
|
+
This is an implementation foundation, not a hosted exchange. Production
|
|
8
|
+
operators must still provide TLS termination, secret management, database
|
|
9
|
+
backups, observability, access control, retention policies, and deployment
|
|
10
|
+
hardening.
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
- SQLite persistence for local/single-node use and a Postgres backend for multi-node production; replaceable through the `Store` boundary.
|
|
15
|
+
- HTTP intake for `lead`, `call`, `bid`, and lifecycle `event` messages.
|
|
16
|
+
- Declarative publisher form mapping with versioned brand/flow normalization and audit digests.
|
|
17
|
+
- Encrypted out-of-band attachment upload/download for contracts, evidence, and call records, with a local AES-GCM backend and production S3-compatible/SSE-KMS adapter, residency controls, and fail-closed ClamAV scanning.
|
|
18
|
+
- Structured call payable evaluation and per-offer monthly quota reporting.
|
|
19
|
+
- JSON Schema and strict ping-safe validation.
|
|
20
|
+
- Bearer API-key and HMAC authentication.
|
|
21
|
+
- Deterministic offer matching and auction selection.
|
|
22
|
+
- At-least-once buyer webhook delivery.
|
|
23
|
+
- Standalone delivery worker for multi-process deployments.
|
|
24
|
+
- WSGI entry point for a production HTTP process manager.
|
|
25
|
+
- Admin CLI for credentials, offers, and controlled lead erasure.
|
|
26
|
+
|
|
27
|
+
See [the implementation decisions](../../docs/IMPLEMENTATION-DECISIONS.md) for
|
|
28
|
+
matching, signing, retry, and sandbox behavior. See the
|
|
29
|
+
[supply-chain security guide](../../docs/SUPPLY-CHAIN-SECURITY.md) for package,
|
|
30
|
+
image, and release controls, and the [container verification guide](../../docs/CONTAINER-SUPPLY-CHAIN.md)
|
|
31
|
+
for signing, provenance, and admission enforcement.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
From the repository root:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Local/single-node profile.
|
|
39
|
+
python3 -m pip install -e implementations/reference-platform
|
|
40
|
+
export LCP_SCHEMA_DIR="$PWD/schemas"
|
|
41
|
+
export LCP_DATABASE_PATH="$PWD/data/lcp.sqlite3"
|
|
42
|
+
# Generate once, store securely, and reuse for this database:
|
|
43
|
+
# python3 -c 'import base64,secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())'
|
|
44
|
+
export LCP_PII_ENCRYPTION_KEY="<persisted-urlsafe-base64-32-byte-key>"
|
|
45
|
+
|
|
46
|
+
# Production profile.
|
|
47
|
+
python3 -m pip install -e 'implementations/reference-platform[production]'
|
|
48
|
+
export LCP_DATABASE_URL='postgresql://lcp_app:<password>@postgres.internal:5432/lcp?sslmode=verify-full'
|
|
49
|
+
export LCP_PII_ENCRYPTION_KEY='<urlsafe-base64-32-byte-key>'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The package targets Python 3.10+. The `[production]` extra adds Gunicorn, Psycopg, boto3, and the ClamAV client
|
|
53
|
+
for the Postgres/WSGI/object-storage deployment profile. Configure
|
|
54
|
+
`LCP_ATTACHMENT_DIRECTORY`, `LCP_MAX_ATTACHMENT_BYTES`, and
|
|
55
|
+
`LCP_ALLOWED_ATTACHMENT_CONTENT_TYPES` for the reference file backend. For
|
|
56
|
+
multi-node production, use `LCP_ATTACHMENT_BACKEND=s3` with an SSE-KMS key,
|
|
57
|
+
explicit residency, and a private ClamAV service; see
|
|
58
|
+
[the attachment deployment guide](../../docs/MVA-ATTACHMENTS.md).
|
|
59
|
+
|
|
60
|
+
## Validate a real Postgres deployment
|
|
61
|
+
|
|
62
|
+
The repository's normal CI runs the reference-platform suite against a fresh
|
|
63
|
+
Postgres 16 service, including encrypted intake, duplicate idempotency, and
|
|
64
|
+
privacy erasure. To run the same test locally, provision an isolated
|
|
65
|
+
throwaway database, install the Postgres extra, and set its URL:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 -m pip install -e 'implementations/reference-platform[production]'
|
|
69
|
+
export LCP_TEST_POSTGRES_URL='postgresql://lcp_test:<password>@127.0.0.1:5432/lcp_test'
|
|
70
|
+
PYTHONPATH=implementations/reference-platform \
|
|
71
|
+
python3 -m unittest discover -s implementations/reference-platform/tests -v
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Never point the integration test at a shared or production database. The test
|
|
75
|
+
creates its own uniquely named tenants and IDs but does not perform a full
|
|
76
|
+
schema reset.
|
|
77
|
+
|
|
78
|
+
## Configure credentials and offers
|
|
79
|
+
|
|
80
|
+
Do not put production credentials in source control. For local setup, add a
|
|
81
|
+
publisher and buyer credential with least-privilege scopes:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
lcp-platform-admin credential upsert \
|
|
85
|
+
--sender-id publisher_001 \
|
|
86
|
+
--tenant-id publisher_tenant \
|
|
87
|
+
--scope lead:submit --scope lead:read \
|
|
88
|
+
--hmac-secret publisher-secret
|
|
89
|
+
|
|
90
|
+
lcp-platform-admin credential upsert \
|
|
91
|
+
--sender-id buyer_001 \
|
|
92
|
+
--tenant-id buyer_tenant \
|
|
93
|
+
--scope bid:submit --scope lead:read \
|
|
94
|
+
--hmac-secret buyer-secret
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Create an offer file:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"offer_id": "mortgage-au",
|
|
102
|
+
"buyer_id": "buyer_001",
|
|
103
|
+
"active": true,
|
|
104
|
+
"routing_mode": "auction",
|
|
105
|
+
"vertical": "mortgage",
|
|
106
|
+
"countries": ["AU"],
|
|
107
|
+
"floor_price_cents": 1500,
|
|
108
|
+
"currency": "AUD",
|
|
109
|
+
"require_consent_evidence": true,
|
|
110
|
+
"require_verified_phone": true,
|
|
111
|
+
"reject_incentivized": true,
|
|
112
|
+
"max_spam_risk_score": 25,
|
|
113
|
+
"daily_cap": 100,
|
|
114
|
+
"ping_timeout_seconds": 30,
|
|
115
|
+
"webhook_url": "https://buyer.example/lcp/webhook"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Validate and store it:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
lcp-platform-admin offer upsert --file offer.json
|
|
123
|
+
|
|
124
|
+
# Controlled privacy operation; verify the legal request and retain the audit record.
|
|
125
|
+
lcp-platform-admin privacy erase-lead --lead-id lead_abc123 --actor-id privacy_operator
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Offer management is intentionally an operator/admin concern. The public LCP
|
|
129
|
+
surface exposes active offers through `GET /v1/lcp/offers`.
|
|
130
|
+
|
|
131
|
+
## Run locally
|
|
132
|
+
|
|
133
|
+
For a single-process local deployment, the HTTP server and delivery worker run
|
|
134
|
+
together:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export LCP_REQUIRE_AUTH=true
|
|
138
|
+
# Keep this key in a secret manager for any non-test deployment.
|
|
139
|
+
# Generate once, store securely, and reuse for this database:
|
|
140
|
+
# python3 -c 'import base64,secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())'
|
|
141
|
+
export LCP_PII_ENCRYPTION_KEY="<persisted-urlsafe-base64-32-byte-key>"
|
|
142
|
+
lcp-platform
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The default listener is `127.0.0.1:8080`. Configure `LCP_HOST`, `LCP_PORT`,
|
|
146
|
+
and the other settings in `lcp_platform/config.py` through environment
|
|
147
|
+
variables.
|
|
148
|
+
|
|
149
|
+
## Production process layout
|
|
150
|
+
|
|
151
|
+
For a production deployment, run the HTTP application and delivery worker
|
|
152
|
+
under a process supervisor:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
TLS reverse proxy
|
|
156
|
+
↓
|
|
157
|
+
WSGI/HTTP application processes ←→ shared database
|
|
158
|
+
↑
|
|
159
|
+
Delivery worker process
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The WSGI entry point is `lcp_platform.wsgi:application`. With the
|
|
163
|
+
`[production]` extra, a typical process command is:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
gunicorn --bind 0.0.0.0:8080 --workers 3 --threads 4 \
|
|
167
|
+
--access-logfile - lcp_platform.wsgi:application
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Run `lcp-platform-worker` as a separate supervised process. Place the
|
|
171
|
+
application behind TLS; the included threaded server is intended for local
|
|
172
|
+
operation and smoke tests.
|
|
173
|
+
|
|
174
|
+
SQLite is appropriate for a single-node deployment or controlled starting
|
|
175
|
+
point. Use a compatible production store implementation when operating at
|
|
176
|
+
multiple nodes or high throughput.
|
|
177
|
+
|
|
178
|
+
## HTTP endpoints
|
|
179
|
+
|
|
180
|
+
| Method | Path | Purpose |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `POST` | `/v1/lcp/leads` | Publisher lead intake (direct or offer-routed) |
|
|
183
|
+
| `POST` | `/v1/lcp/calls` | Publisher call intake |
|
|
184
|
+
| `POST` | `/v1/lcp/bids` | Buyer bid submission |
|
|
185
|
+
| `POST` | `/v1/lcp/events` | Call outcomes and lifecycle/payable updates |
|
|
186
|
+
| `POST` | `/v1/lcp/attachments` | Authenticated scanned/encrypted binary upload |
|
|
187
|
+
| `GET` | `/v1/lcp/attachments/{attachment_id}` | Authenticated attachment download |
|
|
188
|
+
| `GET` | `/v1/lcp/offers/{offer_id}/quota` | Monthly payable quota and pacing report |
|
|
189
|
+
| `GET` | `/v1/lcp/leads/{lead_id}` | Lead status and lifecycle |
|
|
190
|
+
| `GET` | `/health/live` | Unauthenticated process liveness |
|
|
191
|
+
| `GET` | `/health/ready` | Database-backed readiness |
|
|
192
|
+
| `GET` | `/v1/lcp/capabilities` | Capability discovery |
|
|
193
|
+
| `GET` | `/v1/lcp/offers` | Active offer discovery |
|
|
194
|
+
| `GET` | `/v1/lcp/schemas/{name}` | Schema discovery |
|
|
195
|
+
|
|
196
|
+
The complete transport contract is [api/lcp-openapi.yaml](../../api/lcp-openapi.yaml).
|
|
197
|
+
|
|
198
|
+
## Tenant and credential model
|
|
199
|
+
|
|
200
|
+
Every sender has a tenant ID and scopes. Examples include `lead:submit`,
|
|
201
|
+
`lead:read`, `bid:submit`, `event:submit`, `attachment:write`, `offer:read`,
|
|
202
|
+
and `platform:admin`. Lead status is
|
|
203
|
+
only visible to the submitting sender, an authorized buyer that received the
|
|
204
|
+
lead, or a platform administrator. Offers are routed within the configured
|
|
205
|
+
`LCP_ROUTING_TENANT_ID` and are not writable through participant endpoints.
|
|
206
|
+
|
|
207
|
+
Production credentials should be mounted from a secret manager using
|
|
208
|
+
`LCP_SECRETS_FILE`; the database-backed credential path is intended for local
|
|
209
|
+
administration. Production startup also requires `LCP_PII_ENCRYPTION_KEY`, a
|
|
210
|
+
URL-safe base64 encoding of a random 32-byte AES-GCM key. Persisted lead,
|
|
211
|
+
ping, bid, post, and event envelopes are encrypted and authenticated before
|
|
212
|
+
being written to SQLite or Postgres. Store that key in a KMS/HSM-backed secret
|
|
213
|
+
manager and plan an explicit re-encryption migration before rotating it.
|
|
214
|
+
|
|
215
|
+
## Authentication
|
|
216
|
+
|
|
217
|
+
HMAC requests use:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
<timestamp>\n<idempotency-key>\n<raw-request-body>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The exact byte sequence is signed with HMAC-SHA256. Requests carry
|
|
224
|
+
`X-LCP-Timestamp`, `X-LCP-Idempotency-Key`, `X-LCP-Signature`, and
|
|
225
|
+
`X-LCP-Sender-Id`. The default replay window is five minutes.
|
|
226
|
+
|
|
227
|
+
Bearer requests use `Authorization: Bearer <token>` and still carry the
|
|
228
|
+
sender and idempotency headers for mutating operations.
|
|
229
|
+
|
|
230
|
+
## Package publishing
|
|
231
|
+
|
|
232
|
+
A repository-level GitHub Actions workflow can publish the SDK, MCP adapter,
|
|
233
|
+
and reference platform to PyPI through Trusted Publishing on `v*` tags. Each
|
|
234
|
+
package must first be configured as a PyPI Trusted Publisher; no long-lived
|
|
235
|
+
PyPI token is stored in the repository.
|
|
236
|
+
|
|
237
|
+
## Sandbox parity
|
|
238
|
+
|
|
239
|
+
The Docker sandbox uses this same platform package, schemas, matcher, auth,
|
|
240
|
+
and delivery worker. It uses a separate test database and should only receive
|
|
241
|
+
synthetic test messages marked with `X-LCP-Test: true` and envelope `test: true`.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
docker compose -f implementations/reference-platform/docker-compose.yml up --build
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
See [examples/sandbox/README.md](../../examples/sandbox/README.md) for a
|
|
248
|
+
publisher/offer/buyer smoke test.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
services:
|
|
2
|
+
lcp-platform-sandbox:
|
|
3
|
+
build:
|
|
4
|
+
context: ../..
|
|
5
|
+
dockerfile: implementations/reference-platform/Dockerfile
|
|
6
|
+
environment:
|
|
7
|
+
LCP_DATABASE_PATH: /app/data/sandbox.sqlite3
|
|
8
|
+
LCP_REQUIRE_AUTH: "false"
|
|
9
|
+
LCP_TEST_MODE: "true"
|
|
10
|
+
LCP_ALLOW_INSECURE_WEBHOOKS: "true"
|
|
11
|
+
LCP_PLATFORM_ID: sandbox_platform
|
|
12
|
+
LCP_SCHEMA_DIR: /app/schemas
|
|
13
|
+
command: >-
|
|
14
|
+
sh -c "lcp-platform-admin credential upsert --sender-id buyer_sandbox
|
|
15
|
+
--hmac-secret sandbox-buyer-secret &&
|
|
16
|
+
lcp-platform-admin offer upsert --file /app/examples/sandbox/offer.json &&
|
|
17
|
+
lcp-platform"
|
|
18
|
+
ports:
|
|
19
|
+
- "8080:8080"
|
|
20
|
+
volumes:
|
|
21
|
+
- lcp-sandbox-data:/app/data
|
|
22
|
+
|
|
23
|
+
buyer-sandbox:
|
|
24
|
+
build:
|
|
25
|
+
context: ../..
|
|
26
|
+
dockerfile: implementations/reference-platform/Dockerfile
|
|
27
|
+
environment:
|
|
28
|
+
LCP_PLATFORM_URL: http://lcp-platform-sandbox:8080
|
|
29
|
+
command: ["python", "/app/examples/sandbox/buyer.py"]
|
|
30
|
+
depends_on:
|
|
31
|
+
- lcp-platform-sandbox
|
|
32
|
+
|
|
33
|
+
volumes:
|
|
34
|
+
lcp-sandbox-data:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Kubernetes deployment example
|
|
2
|
+
|
|
3
|
+
These manifests demonstrate a cloud-neutral Kubernetes shape for the LCP
|
|
4
|
+
reference platform. They are intentionally not a complete turnkey production
|
|
5
|
+
cluster configuration.
|
|
6
|
+
|
|
7
|
+
## Before applying
|
|
8
|
+
|
|
9
|
+
1. Build and publish the reference-platform image to your registry.
|
|
10
|
+
2. Provide a supported Postgres service; do not run the database in this
|
|
11
|
+
example for production.
|
|
12
|
+
3. Provide an approved RWX attachment volume or replace the reference file
|
|
13
|
+
backend with a residency-controlled object-store adapter.
|
|
14
|
+
4. Copy `secret.example.yaml` to a private, access-controlled Secret manifest
|
|
15
|
+
or use External Secrets/KMS integration.
|
|
16
|
+
5. Replace the example image, database URL, tenant ID, resource limits,
|
|
17
|
+
namespaces, network policies, ingress, and residency controls.
|
|
18
|
+
6. Configure a production WSGI server or adapt the image entry point to the
|
|
19
|
+
organization's process manager.
|
|
20
|
+
7. Publish the image through the signed container workflow, resolve its digest,
|
|
21
|
+
and replace the example image reference with that digest.
|
|
22
|
+
8. Add a TLS ingress/WAF and private egress policy.
|
|
23
|
+
|
|
24
|
+
## Apply
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
kubectl create namespace lcp
|
|
28
|
+
kubectl apply -n lcp -f secret.example.yaml # replace with a real secret workflow
|
|
29
|
+
kubectl apply -n lcp -f pvc.example.yaml # use approved RWX storage or an object-store adapter
|
|
30
|
+
kubectl apply -n lcp -f configmap.yaml
|
|
31
|
+
kubectl apply -n lcp -f deployment.yaml
|
|
32
|
+
kubectl apply -n lcp -f service.yaml
|
|
33
|
+
kubectl apply -n lcp -f pod-disruption-budget.yaml
|
|
34
|
+
# Optional; tune against queue age and database capacity before applying.
|
|
35
|
+
kubectl apply -n lcp -f hpa.example.yaml
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The API and worker are separate deployments. Both use the same image and
|
|
39
|
+
configuration; only the entry point differs. The worker needs access to the
|
|
40
|
+
same Postgres database but does not need a public Service.
|
|
41
|
+
|
|
42
|
+
## Production additions
|
|
43
|
+
|
|
44
|
+
- `pvc.example.yaml` for the reference encrypted attachment directory. A
|
|
45
|
+
multi-zone production deployment should prefer an object-store adapter with
|
|
46
|
+
customer-managed encryption and explicit residency controls.
|
|
47
|
+
- `network-policy.example.yaml` restricting database and buyer egress.
|
|
48
|
+
- `pod-disruption-budget.yaml` and multi-zone scheduling.
|
|
49
|
+
- `hpa.example.yaml`, extended with queue-age and database-capacity signals.
|
|
50
|
+
- External Secrets/KMS and key rotation, including controlled re-encryption of persisted envelopes.
|
|
51
|
+
- Ingress TLS, WAF, DDoS protection, and request-size limits.
|
|
52
|
+
- Postgres HA, backups, encryption, and restore drills.
|
|
53
|
+
- Centralized redacted logs, metrics, traces, and alerts.
|
|
54
|
+
- Image signing, admission policy, SBOM verification, and vulnerability scans.
|
|
55
|
+
- `verify-images-kyverno.example.yaml` — optional Kyverno enforcement for
|
|
56
|
+
keyless signatures, SLSA provenance, and CycloneDX SBOM attestations.
|
|
57
|
+
- `tests/verify-images/` — CI fixtures proving unsigned, wrong-workflow,
|
|
58
|
+
missing-provenance, and missing-SBOM images are rejected.
|
|
59
|
+
|
|
60
|
+
See [container signing and provenance verification](../../../docs/CONTAINER-SUPPLY-CHAIN.md)
|
|
61
|
+
for Cosign/GitHub verification commands and cloud-neutral admission guidance.
|
|
62
|
+
|
|
63
|
+
## Policy test fixtures
|
|
64
|
+
|
|
65
|
+
The `tests/verify-images/` fixture uses Kyverno's public verification-test
|
|
66
|
+
images and expects rejection for four negative cases:
|
|
67
|
+
|
|
68
|
+
- an unsigned image;
|
|
69
|
+
- an image signed by a different workflow/identity;
|
|
70
|
+
- an image without the required SLSA provenance attestation; and
|
|
71
|
+
- an image without the required CycloneDX SBOM attestation.
|
|
72
|
+
|
|
73
|
+
Run it with the Kyverno CLI and registry access:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
kyverno test implementations/reference-platform/kubernetes/tests/verify-images \
|
|
77
|
+
--registry --detailed-results
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The GitHub test workflow installs Kyverno CLI `v1.18.2`, verifies its pinned
|
|
81
|
+
Linux archive with the release's Sigstore bundle, and then runs this fixture.
|
|
82
|
+
Keep that version and verification identity under review when upgrading the
|
|
83
|
+
policy engine or its CLI.
|
|
84
|
+
|
|
85
|
+
The fixture is a policy-behavior test, not a substitute for a live admission
|
|
86
|
+
test against the organization's registry, Kyverno version, credentials, and
|
|
87
|
+
network policy.
|
|
88
|
+
|
|
89
|
+
The CI `admission` job also creates a disposable, pinned kind cluster, installs
|
|
90
|
+
Kyverno from a digest-checked release manifest, applies these policies in
|
|
91
|
+
`Enforce` mode, and submits the four fixtures through the Kubernetes API server
|
|
92
|
+
with server-side dry runs. It fails unless Kyverno rejects the unsigned,
|
|
93
|
+
wrong-workflow, missing-provenance, and missing-SBOM cases through the live
|
|
94
|
+
admission webhook.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: ConfigMap
|
|
3
|
+
metadata:
|
|
4
|
+
name: lcp-platform-config
|
|
5
|
+
data:
|
|
6
|
+
LCP_SCHEMA_DIR: /app/schemas
|
|
7
|
+
LCP_REQUIRE_AUTH: "true"
|
|
8
|
+
LCP_ALLOW_INSECURE_WEBHOOKS: "false"
|
|
9
|
+
LCP_PLATFORM_ID: platform_001
|
|
10
|
+
LCP_ROUTING_TENANT_ID: default
|
|
11
|
+
LCP_REPLAY_WINDOW_SECONDS: "300"
|
|
12
|
+
LCP_MAX_BODY_BYTES: "2000000"
|
|
13
|
+
LCP_MAX_HEADER_BYTES: "16384"
|
|
14
|
+
LCP_MAX_WEBHOOK_RESPONSE_BYTES: "262144"
|
|
15
|
+
LCP_RATE_LIMIT_PER_MINUTE: "600"
|
|
16
|
+
LCP_WEBHOOK_TIMEOUT_SECONDS: "10"
|
|
17
|
+
LCP_MAX_DELIVERY_ATTEMPTS: "5"
|
|
18
|
+
LCP_WORKER_INTERVAL_SECONDS: "1"
|
|
19
|
+
LCP_ATTACHMENT_DIRECTORY: /var/lib/lcp/attachments
|
|
20
|
+
LCP_MAX_ATTACHMENT_BYTES: "104857600"
|
|
21
|
+
LCP_ALLOWED_ATTACHMENT_CONTENT_TYPES: application/pdf,image/jpeg,image/png,image/webp,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: lcp-platform-api
|
|
5
|
+
spec:
|
|
6
|
+
replicas: 3
|
|
7
|
+
selector:
|
|
8
|
+
matchLabels:
|
|
9
|
+
app: lcp-platform-api
|
|
10
|
+
template:
|
|
11
|
+
metadata:
|
|
12
|
+
labels:
|
|
13
|
+
app: lcp-platform-api
|
|
14
|
+
spec:
|
|
15
|
+
serviceAccountName: lcp-platform
|
|
16
|
+
securityContext:
|
|
17
|
+
runAsNonRoot: true
|
|
18
|
+
fsGroup: 10001
|
|
19
|
+
seccompProfile:
|
|
20
|
+
type: RuntimeDefault
|
|
21
|
+
containers:
|
|
22
|
+
- name: api
|
|
23
|
+
image: ghcr.io/replace-me/lcp-reference-platform@sha256:REPLACE_WITH_SIGNED_DIGEST
|
|
24
|
+
imagePullPolicy: IfNotPresent
|
|
25
|
+
command: ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "3", "--threads", "4", "--access-logfile", "-", "lcp_platform.wsgi:application"]
|
|
26
|
+
ports:
|
|
27
|
+
- name: http
|
|
28
|
+
containerPort: 8080
|
|
29
|
+
envFrom:
|
|
30
|
+
- configMapRef:
|
|
31
|
+
name: lcp-platform-config
|
|
32
|
+
env:
|
|
33
|
+
- name: LCP_DATABASE_URL
|
|
34
|
+
valueFrom:
|
|
35
|
+
secretKeyRef:
|
|
36
|
+
name: lcp-platform-secrets
|
|
37
|
+
key: LCP_DATABASE_URL
|
|
38
|
+
- name: LCP_PII_ENCRYPTION_KEY
|
|
39
|
+
valueFrom:
|
|
40
|
+
secretKeyRef:
|
|
41
|
+
name: lcp-platform-secrets
|
|
42
|
+
key: LCP_PII_ENCRYPTION_KEY
|
|
43
|
+
- name: LCP_SECRETS_FILE
|
|
44
|
+
value: /run/secrets/lcp/credentials.json
|
|
45
|
+
volumeMounts:
|
|
46
|
+
- name: lcp-credentials
|
|
47
|
+
mountPath: /run/secrets/lcp
|
|
48
|
+
readOnly: true
|
|
49
|
+
- name: lcp-attachments
|
|
50
|
+
mountPath: /var/lib/lcp/attachments
|
|
51
|
+
readinessProbe:
|
|
52
|
+
httpGet:
|
|
53
|
+
path: /health/ready
|
|
54
|
+
port: http
|
|
55
|
+
initialDelaySeconds: 5
|
|
56
|
+
periodSeconds: 10
|
|
57
|
+
livenessProbe:
|
|
58
|
+
httpGet:
|
|
59
|
+
path: /health/live
|
|
60
|
+
port: http
|
|
61
|
+
initialDelaySeconds: 15
|
|
62
|
+
periodSeconds: 20
|
|
63
|
+
resources:
|
|
64
|
+
requests:
|
|
65
|
+
cpu: 250m
|
|
66
|
+
memory: 256Mi
|
|
67
|
+
limits:
|
|
68
|
+
cpu: "2"
|
|
69
|
+
memory: 1Gi
|
|
70
|
+
securityContext:
|
|
71
|
+
allowPrivilegeEscalation: false
|
|
72
|
+
readOnlyRootFilesystem: true
|
|
73
|
+
capabilities:
|
|
74
|
+
drop: ["ALL"]
|
|
75
|
+
volumes:
|
|
76
|
+
- name: lcp-credentials
|
|
77
|
+
secret:
|
|
78
|
+
secretName: lcp-platform-secrets
|
|
79
|
+
defaultMode: 0440
|
|
80
|
+
items:
|
|
81
|
+
- key: LCP_CREDENTIALS_JSON
|
|
82
|
+
path: credentials.json
|
|
83
|
+
- name: lcp-attachments
|
|
84
|
+
persistentVolumeClaim:
|
|
85
|
+
claimName: lcp-attachments
|
|
86
|
+
---
|
|
87
|
+
apiVersion: apps/v1
|
|
88
|
+
kind: Deployment
|
|
89
|
+
metadata:
|
|
90
|
+
name: lcp-platform-worker
|
|
91
|
+
spec:
|
|
92
|
+
replicas: 2
|
|
93
|
+
selector:
|
|
94
|
+
matchLabels:
|
|
95
|
+
app: lcp-platform-worker
|
|
96
|
+
template:
|
|
97
|
+
metadata:
|
|
98
|
+
labels:
|
|
99
|
+
app: lcp-platform-worker
|
|
100
|
+
spec:
|
|
101
|
+
serviceAccountName: lcp-platform
|
|
102
|
+
securityContext:
|
|
103
|
+
runAsNonRoot: true
|
|
104
|
+
fsGroup: 10001
|
|
105
|
+
seccompProfile:
|
|
106
|
+
type: RuntimeDefault
|
|
107
|
+
containers:
|
|
108
|
+
- name: worker
|
|
109
|
+
image: ghcr.io/replace-me/lcp-reference-platform@sha256:REPLACE_WITH_SIGNED_DIGEST
|
|
110
|
+
imagePullPolicy: IfNotPresent
|
|
111
|
+
command: ["lcp-platform-worker"]
|
|
112
|
+
envFrom:
|
|
113
|
+
- configMapRef:
|
|
114
|
+
name: lcp-platform-config
|
|
115
|
+
env:
|
|
116
|
+
- name: LCP_DATABASE_URL
|
|
117
|
+
valueFrom:
|
|
118
|
+
secretKeyRef:
|
|
119
|
+
name: lcp-platform-secrets
|
|
120
|
+
key: LCP_DATABASE_URL
|
|
121
|
+
- name: LCP_PII_ENCRYPTION_KEY
|
|
122
|
+
valueFrom:
|
|
123
|
+
secretKeyRef:
|
|
124
|
+
name: lcp-platform-secrets
|
|
125
|
+
key: LCP_PII_ENCRYPTION_KEY
|
|
126
|
+
- name: LCP_SECRETS_FILE
|
|
127
|
+
value: /run/secrets/lcp/credentials.json
|
|
128
|
+
volumeMounts:
|
|
129
|
+
- name: lcp-credentials
|
|
130
|
+
mountPath: /run/secrets/lcp
|
|
131
|
+
readOnly: true
|
|
132
|
+
- name: lcp-attachments
|
|
133
|
+
mountPath: /var/lib/lcp/attachments
|
|
134
|
+
resources:
|
|
135
|
+
requests:
|
|
136
|
+
cpu: 250m
|
|
137
|
+
memory: 256Mi
|
|
138
|
+
limits:
|
|
139
|
+
cpu: "2"
|
|
140
|
+
memory: 1Gi
|
|
141
|
+
securityContext:
|
|
142
|
+
allowPrivilegeEscalation: false
|
|
143
|
+
readOnlyRootFilesystem: true
|
|
144
|
+
capabilities:
|
|
145
|
+
drop: ["ALL"]
|
|
146
|
+
volumes:
|
|
147
|
+
- name: lcp-credentials
|
|
148
|
+
secret:
|
|
149
|
+
secretName: lcp-platform-secrets
|
|
150
|
+
defaultMode: 0440
|
|
151
|
+
items:
|
|
152
|
+
- key: LCP_CREDENTIALS_JSON
|
|
153
|
+
path: credentials.json
|
|
154
|
+
- name: lcp-attachments
|
|
155
|
+
persistentVolumeClaim:
|
|
156
|
+
claimName: lcp-attachments
|
|
157
|
+
---
|
|
158
|
+
apiVersion: v1
|
|
159
|
+
kind: ServiceAccount
|
|
160
|
+
metadata:
|
|
161
|
+
name: lcp-platform
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apiVersion: autoscaling/v2
|
|
2
|
+
kind: HorizontalPodAutoscaler
|
|
3
|
+
metadata:
|
|
4
|
+
name: lcp-platform-api
|
|
5
|
+
spec:
|
|
6
|
+
scaleTargetRef:
|
|
7
|
+
apiVersion: apps/v1
|
|
8
|
+
kind: Deployment
|
|
9
|
+
name: lcp-platform-api
|
|
10
|
+
minReplicas: 3
|
|
11
|
+
maxReplicas: 50
|
|
12
|
+
behavior:
|
|
13
|
+
scaleDown:
|
|
14
|
+
stabilizationWindowSeconds: 300
|
|
15
|
+
metrics:
|
|
16
|
+
- type: Resource
|
|
17
|
+
resource:
|
|
18
|
+
name: cpu
|
|
19
|
+
target:
|
|
20
|
+
type: Utilization
|
|
21
|
+
averageUtilization: 65
|
|
22
|
+
---
|
|
23
|
+
apiVersion: autoscaling/v2
|
|
24
|
+
kind: HorizontalPodAutoscaler
|
|
25
|
+
metadata:
|
|
26
|
+
name: lcp-platform-worker
|
|
27
|
+
spec:
|
|
28
|
+
scaleTargetRef:
|
|
29
|
+
apiVersion: apps/v1
|
|
30
|
+
kind: Deployment
|
|
31
|
+
name: lcp-platform-worker
|
|
32
|
+
minReplicas: 2
|
|
33
|
+
maxReplicas: 50
|
|
34
|
+
behavior:
|
|
35
|
+
scaleDown:
|
|
36
|
+
stabilizationWindowSeconds: 300
|
|
37
|
+
metrics:
|
|
38
|
+
- type: Resource
|
|
39
|
+
resource:
|
|
40
|
+
name: cpu
|
|
41
|
+
target:
|
|
42
|
+
type: Utilization
|
|
43
|
+
averageUtilization: 65
|