01 What you are deploying
Axia is a security-event analysis platform. It ingests events from data sources (Microsoft 365 / Outlook, Microsoft Purview DLP, Cursor and other webhook sources, API), enriches and classifies them, detects sensitive-data exposure with an LLM, and surfaces cases in a web dashboard. Integration setup for Microsoft Purview and Cursor is in §9.
Target architecture (per AWS account)
AWS Account · region of your choice
┌──────────────────────────────────────────────────────────────────────────┐
│ VPC 10.0.0.0/16 · 3 AZs · 3 public /20 · 3 private /20 · 1 NAT │
│ │
│ ┌── private subnet ───────────────┐ ┌── private subnet ────────────┐ │
│ │ Platform EC2 (t3a.large) │ │ Classifier / ML EC2 │ │
│ │ Ubuntu 22.04 + Docker │ │ (c7i-flex.4xlarge) │ │
│ │ compose: kafka · redis · │◄─►│ compose: llm-service · │ │
│ │ object-storage · pipeline · │ │ parser-service · rapidocr · │ │
│ │ domain services · app layer │ │ data-classification │ │
│ └───────────┬─────────────────────┘ └──────────────┬───────────────┘ │
│ │ 27017 (TLS) │ │
│ ┌──────▼──────────┐ ┌────────▼────────┐ │
│ │ DocumentDB │ │ AWS Bedrock │ │
│ │ (MongoDB-compat) │ │ (Claude models) │ │
│ └──────────────────┘ └──────────────────┘ │
│ ┌──────────────────┐ Hosts reachable only via SSM Session Manager │
│ │ S3 content-store │ (no public SSH — EC2 lives in private subnets)│
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
▲ images pulled cross-account from Axia ECR
└ <AXIA_ECR_REGISTRY>/<service>:<tag> (registry provided by Axia)
Two hosts
The full product runs across two EC2 instances, each with its own docker-compose file that communicate over private-IP URLs.
Platform EC2 compose/platform/docker-compose.yml
Kafka, Redis, object-storage, the ingestion pipeline (connectors + enrichment workers + event-analyzer + webhook), domain services (case, chat, events, engagement), and the app layer (application-backend/frontend, legacy app-backend/dashboard).
Classifier / ML EC2 compose/ml/docker-compose.yml
llm-service (Bedrock gateway), parser-service, rapidocr (OCR), and data-classification. Wired to the platform host via LLM_SERVICE_URL, PARSER_SERVICE_URL, and KAFKA_ADVERTISED_HOST.
Terraform vs. the running app
Terraform only builds AWS infrastructure; it never starts the application. The application layer runs as docker-compose on the hosts, configured from Terraform outputs.
| Terraform provisions | Application layer |
|---|---|
| VPC, subnets, NAT, security groups | Kafka, Redis, all Axia service containers |
| Platform EC2 + Classifier EC2 (Docker pre-installed) | Started with docker compose up -d over SSM |
| DocumentDB cluster (the "MongoDB") | Config from TF outputs + client-config.env |
| S3 content-store bucket + credentials | |
| IAM roles (ECR pull, Bedrock, SSM, CloudWatch, S3) | |
| CloudWatch log groups |
02 Prerequisites
The deployment bundle
Axia ships a self-contained deployment bundle — you do not clone the source repos. Unpack it and run every command below from inside it:
axia-deploy-<version>/ ├── terraform/ │ ├── init/ # terraform-backend.yaml, github-actions-role.yaml (bootstrap) │ ├── vpc/ # network stack │ ├── platform/ # EC2 + DocumentDB + S3 + IAM (+ example.backend.hcl, example.tfvars) │ └── scripts/ # generate-deployment-config.sh, deploy.sh, ECR-cross-account-trust.sh ├── compose/ │ ├── platform/docker-compose.yml # platform host services │ └── ml/docker-compose.yml # ML/classifier host services └── client-config.env.template
The apps-ci/ and tenants/ Terraform stacks (they run only in Axia's ECR-hosting account), Axia's own per-environment *.tfvars/*.backend.hcl and account IDs, the GitHub Actions CI workflows, and repo history. The bundle's platform/ stack ships an example.backend.hcl and example.tfvars to copy; the scripts are pre-fixed to bundle-relative paths.
Operator workstation tooling
| Tool | Purpose |
|---|---|
aws CLI v2 | bootstrap, Terraform auth, SSM sessions, reading outputs |
| Terraform ≥ 1.0 (AWS provider ≥ 6.10.0) | provisioning |
jq | generating deployment config from TF outputs |
python3 | password URL-encoding in the config generator |
| Docker | local testing only |
unzip | unpacking the Axia deployment bundle |
| Session Manager plugin | connecting to the private-subnet hosts |
AWS prerequisites
- A dedicated AWS account for this deployment (each environment = its own account).
- Admin access for the one-time bootstrap, then the scoped deployment role for applies.
- Bedrock model access enabled in the region for the Claude models used by
llm-serviceandchat-service. - Pick one region and use it consistently across the whole deployment.
Container images live in Axia's ECR and are pulled cross-account. A self-hosted deployment therefore requires Axia to (1) grant cross-account ECR pull for your account, (2) provide the bootstrap IAM policy JSONs (trust.json and the role permission docs — distributed by Axia, not in the repo), and optionally (3) publish tagged image releases.
Data-source credentials (collect before deploy)
- Microsoft 365 / Outlook: Entra app — tenant ID, client ID, client secret (Graph permissions + admin consent), org domain.
- Microsoft Purview DLP: an Entra app with Graph
SecurityAlert.Read.All(can reuse the Outlook app). Full steps in §9.1. - Cursor (user-prompt events): captured via Cursor's local
beforeSubmitPrompthook deployed to developer endpoints, forwarding to the webhook receiver — you set a webhook source secret. Fleet-wide deployment needs Cursor Enterprise. See §9.2. - Webhook / API: no third-party credentials — configure source/topic settings in
client-config.env. - SMTP (optional reviewer-alert email): host / port / user / password / from-address.
03 Phase 0 — AWS account bootstrap
One-time, with admin credentials. Creates the Terraform state backend and the IAM identities Terraform assumes.
Two variants exist. Use the CloudFormation path below — it also creates the GitHub OIDC provider that the platform IAM stack depends on. (The CLI-only variant in CLIENT-AWS-SETUP.md creates just the S3 bucket and expects Axia-supplied role JSONs.)
Set your context
export AWS_REGION=<your-region> export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) echo "Deploying to account ${ACCOUNT_ID} in ${AWS_REGION}"
Create the Terraform state backend (S3 + DynamoDB lock)
aws cloudformation create-stack \
--stack-name terraform-backend \
--template-body file://init/terraform-backend.yaml \
--region ${AWS_REGION}
# confirm
aws cloudformation describe-stacks --stack-name terraform-backend \
--region ${AWS_REGION} --query 'Stacks[0].StackStatus'Yields the state bucket by convention: terraform-state-<ACCOUNT_ID>-<REGION>, versioned with public access blocked.
Create the GitHub OIDC provider + deploy role
aws cloudformation create-stack \
--stack-name terraform-iac-github-actions-role \
--template-body file://init/github-actions-role.yaml \
--region ${AWS_REGION} \
--parameters \
ParameterKey=GitHubOwner,ParameterValue=Axia-Security \
ParameterKey=GitHubRepo,ParameterValue=infra \
ParameterKey=Region,ParameterValue=${AWS_REGION} \
--capabilities CAPABILITY_NAMED_IAM
# get the deploy role ARN
aws cloudformation describe-stacks --stack-name terraform-iac-github-actions-role \
--region ${AWS_REGION} \
--query 'Stacks[0].Outputs[?OutputKey==`RoleArn`].OutputValue' --output textThe OIDC provider (token.actions.githubusercontent.com) must exist before the platform and apps-ci stacks apply — their IAM roles reference it.
04 Phase 1 — Terraform infrastructure
Apply the two bundled stacks in order: vpc/ → platform/. Backend convention for every stack: bucket terraform-state-<ACCOUNT_ID>-<REGION>, key <stack>/terraform.tfstate.
Per-environment config files
Create a backend file per stack (copy the bundled terraform/platform/example.backend.hcl):
bucket = "terraform-state-<ACCOUNT_ID>-<REGION>" key = "platform/terraform.tfstate" region = "<REGION>"
And a <env>.tfvars (copy from the bundled example.tfvars):
enable_alerts = false # true to enable CloudTrail/SNS/CloudWatch alarms roles_allowed_to_assume_platform_role = [ # SSO/admin role names in THIS account allowed to assume the platform EC2 role ]
Other platform variables: region (set to your chosen region), project_name (platform, drives resource names + EC2 Name tag), documentdb_instance_class (db.t3.medium), documentdb_instance_count (1), documentdb_engine_version (8.0.0), classifier_instance_type (c7i-flex.4xlarge), classifier_volume_size (200 GB), and — when enable_alerts = true — alerts_email + alerts_client_name.
Apply the VPC stack
terraform init -reconfigure \ -backend-config="bucket=terraform-state-${ACCOUNT_ID}-${AWS_REGION}" \ -backend-config="key=vpc/terraform.tfstate" \ -backend-config="region=${AWS_REGION}" terraform plan -var="region=${AWS_REGION}" terraform apply -var="region=${AWS_REGION}"
Creates 10.0.0.0/16 with 3 public + 3 private /20 subnets across 3 AZs and a NAT gateway. Its output is consumed by the platform stack via remote state.
Apply the platform stack
terraform init -reconfigure -backend-config=<env>.backend.hcl terraform plan -var-file=<env>.tfvars terraform apply -var-file=<env>.tfvars
Reads the VPC remote state and creates: the Platform EC2 (t3a.large, Ubuntu 22.04, gp3 100 GB encrypted, private subnet, Docker bootstrapped by user_data.sh), the Classifier EC2, the DocumentDB cluster (master docdbadmin, password auto-managed in Secrets Manager, TLS + encryption + 7-day backups + deletion protection), the S3 content-store bucket and access user, all IAM roles/policies, and CloudWatch log groups.
Capture the outputs you need for the app layer:
terraform output ssm_command # connect to platform host terraform output documentdb_cluster_endpoint terraform output documentdb_get_password_command terraform output content_store_bucket_name terraform output s3_content_access_get_credentials_command terraform output classifier_ssm_command # connect to ML host
Axia-side stacks (not in the bundle)
Two further Terraform stacks — apps-ci/ (ECR repositories + shared CI OIDC role) and tenants/ (per-tenant read-only ECR-pull IAM users) — run only in Axia's ECR-hosting account. They are intentionally excluded from the deployment bundle; your account pulls images cross-account instead (§5). No action required from you.
05 Phase 2 — Cross-account ECR access
For a self-hosted account to pull images from Axia's ECR, Axia runs ECR-cross-account-trust.sh (Axia-side), which sets a repository policy on each service repo allowing your account root to BatchCheckLayerAvailability, GetDownloadUrlForLayer, and BatchGetImage.
The platform EC2 IAM role already grants the pull actions; the cross-account repository policy is the missing half that Axia must add for your account ID. Without it, pulls are denied.
06 Phase 3 — Deploy the application layer
The application is delivered onto the EC2 host as a docker-compose stack. Two paths:
- Manual (recommended for self-host): generate config from TF outputs, copy it and the compose file to the host over SSM, run the deploy script.
- CI (Axia-managed): the
platform-deploy.ymlworkflow assumes the deploy role via OIDC, finds the instance byNametag, and runsdocker compose up -dover SSM using GitHub Environment vars/secrets.
Generate the deployment config
../scripts/generate-deployment-config.sh <CLIENT_NAME>
It reads terraform output, fetches the DocumentDB password from Secrets Manager and S3 credentials from SSM, URL-encodes the password, and writes deployment-config-<CLIENT>.json (account, region, ECR registry, DocumentDB endpoint, full TLS mongodb_uri, content-store bucket + region, S3 access/secret keys).
The bundled generate-deployment-config.sh resolves the platform stack relative to the bundle — no path editing required. The ECR registry/region in the output point at the Axia ECR account by design (that is where images are pulled from cross-account, §5).
The MongoDB URI uses DocumentDB TLS form:
mongodb://docdbadmin:<url-encoded-pw>@<endpoint>:27017/axia_security?tls=true &replicaSet=rs0&readPreference=secondaryPreferred&retryWrites=false &tlsCAFile=/app/certs/global-bundle.pem&authMechanism=SCRAM-SHA-1
Create client-config.env
Copy client-config.env.template (bundle root) to client-config.env and fill in the per-client, non-generated values (the data-source credentials from §2). Leave a connector's section commented if you are not enabling it.
Copy files to the host and deploy
$(terraform output -raw ssm_command) # aws ssm start-session --target i-xxxx …Place these three files in ~/axia/ on the host — the deploy script requires all three:
deployment-config.json(from above)client-config.envdocker-compose.yml— use the bundledcompose/platform/docker-compose.yml
Then run the deploy script on the host. It installs jq/python3 if missing, downloads the DocumentDB CA bundle to ~/axia/certs/global-bundle.pem, logs Docker in to ECR, generates ~/axia/.env (mapping config → service env keys, fanning the Mongo URI to the services, setting the Bedrock LLM_MODEL, KAFKA_BOOTSTRAP_SERVERS=kafka:29092, and the S3/bucket vars) and appends your entire client-config.env, then runs docker compose pull and up -d.
./deploy.sh docker compose ps
The script is idempotent — safe to re-run for upgrades.
07 Phase 4 — Deploy the ML / Classifier host
Repeat the deploy on the Classifier EC2 using the bundled compose/ml/docker-compose.yml, which runs llm-service, parser-service, rapidocr, and data-classification.
$(terraform output -raw classifier_ssm_command) # connect to the ML host- Bedrock must be reachable from the instance role, with
AWS_DEFAULT_REGIONset (a missing region is the classicNoRegionErrorforllm-service). - Mount the DocumentDB CA bundle at
/app/certs/global-bundle.pem(data-classificationneeds it). - The ML box reaches platform Kafka via
KAFKA_ADVERTISED_HOST= platform private IP; platform services reach the ML box viaLLM_SERVICE_URL/PARSER_SERVICE_URL= ML private IP.
08 Phase 5 — Verification
docker compose ps # all Up / healthy docker compose logs --tail 100 | grep -iE 'error|exception|failed'
| Component | Port | Check |
|---|---|---|
| dashboard (v1 UI) | 5173 | HTTP 200 on / |
| application-frontend (v2 UI) | 5174 | HTTP 200 on / |
| application-backend (v2 API) | 8088 | /health |
| app-backend (legacy v1) | 8003 | /health |
| object-storage-service | 8080 | / |
| events-service | 8001 | /healthz |
| case-service | 8005 | / |
| chat-service | 8008 | / |
| engagement-service | 8010 | / |
| webhook-connector | 8011 | / |
| kafka-ui | 8081 | web UI |
| llm-service (ML host) | 8000 | /health |
| parser-service (ML host) | 8090 | / |
To reach a UI or DocumentDB from your workstation, use SSM port-forwarding (the platform stack prints ready-made commands):
terraform output documentdb_ssm_port_forward_command # localhost:27017 → DocumentDB09 Data-source integrations
Axia ingests external events through connectors. This section covers the two integrations for this deployment: Microsoft Purview DLP (Axia polls Microsoft Graph outbound) and Cursor user-prompt events (forwarded to Axia's webhook receiver by a hook on each developer's machine). Both are additive — configure only what you use. All values below go in client-config.env (§6.2).
9.1 · Microsoft Purview DLP
How it works. The connector-purview service (the shared connector image with CONNECTOR_TYPE=purview) authenticates to Microsoft with the OAuth2 client-credentials flow and polls the Microsoft Graph Security API for DLP alerts. Each alert is published to the dlp.events Kafka topic, where the existing enrichment-worker-dlp consumes it. The connector reaches out to Microsoft — no inbound network exposure is needed.
GET https://graph.microsoft.com/v1.0/security/alerts_v2
?$filter=serviceSource eq 'dataLossPrevention'
# incremental polls add: and lastUpdateDateTime gt <timestamp>
# results are paged via @odata.nextLinkDLP policies must be configured and enabled in the Microsoft Purview portal with rules set to generate alerts (no policies → no alerts). Single-event alerts work on any DLP subscription; aggregated/threshold alerts require E5 / the Purview compliance tier. New or modified policies can take up to ~3 hours to start generating alerts.
Azure Entra app registration (one-time, in the customer tenant):
- Entra admin center → App registrations → New registration. Name it (e.g. "Axia Purview DLP Connector"), single-tenant, no redirect URI. Register.
- On Overview, copy Application (client) ID →
MICROSOFT_CLIENT_IDand Directory (tenant) ID →MICROSOFT_TENANT_ID. - Certificates & secrets → New client secret → copy the Value (shown once) →
MICROSOFT_CLIENT_SECRET. - API permissions → Add a permission → Microsoft Graph → Application permissions → add
SecurityAlert.Read.All. - Grant admin consent for the tenant — required, or the token won't carry the permission.
You can reuse the same Entra app as the Outlook connector: just add the SecurityAlert.Read.All permission to it and reuse the same tenant/client/secret.
Configure the connector (client-config.env):
MICROSOFT_TENANT_ID=<tenant-id> MICROSOFT_CLIENT_ID=<client-id> MICROSOFT_CLIENT_SECRET=<client-secret> PURVIEW_START_DATE= # optional backfill (e.g. 2026-01-01); blank = from now
Stack-provided defaults you normally leave alone: MICROSOFT_GRAPH_BASE_URL=https://graph.microsoft.com/v1.0, MICROSOFT_AUTHORITY_BASE_URL=https://login.microsoftonline.com, PURVIEW_PENDING_QUEUE_NAME=dlp.events.
Verify. docker compose logs -f connector-purview shows a successful token fetch and poll; DLP alerts land on the dlp.events topic (visible in kafka-ui) and flow through enrichment-worker-dlp into the events service.
Microsoft references: list alerts_v2 · serviceSource enum · client-credentials / .default · DLP alerts prerequisites & licensing.
9.2 · Cursor (user-prompt events)
We ingest only the user-prompt events — the text developers submit to Cursor's AI — for sensitive-data detection.
How it works. Unlike Purview, you don't point Cursor at a URL — Cursor has no cloud-side webhook or API that emits user prompts (its only outbound webhook is Cloud-Agent status; its Admin API, audit logs, and code-tracking exports all exclude prompt text). Prompt capture is done with a local Cursor hook — beforeSubmitPrompt — that runs a small script on each developer's machine right after the user hits send; the script receives the prompt as JSON on stdin and forwards it to Axia's webhook receiver. Because your script makes the call (not Cursor's cloud), it sets the static X-Webhook-Secret header itself, so it's directly compatible with the receiver — no HMAC shim. Enabling only user-prompt events means declaring only the beforeSubmitPrompt hook.
Developer's Cursor ──beforeSubmitPrompt hook──▶ forwarder script ──HTTPS POST──▶ webhook-connector :8011
(prompt text) (adds X-Webhook-Secret) POST /api/v1/webhooks/cursor
→ enrichment.pending
→ enrichment-worker-webhookStep 1 — register the source on the Axia side (client-config.env):
# name:secret pairs, ';'-separated (this is the static X-Webhook-Secret value) WEBHOOK_ENRICHMENT_SOURCES=cursor:<axia-webhook-secret> WEBHOOK_ENRICHMENT_TOPIC=enrichment.pending
Step 2 — expose the receiver to developer machines. The receiver (:8011) sits on the private-subnet platform host, so front it with an HTTPS ingress (ALB / reverse proxy) reachable from where developers run Cursor (corporate network / VPN, or internet for remote staff). Terminate TLS at the ingress; forward POST /api/v1/webhooks/cursor.
Step 3 — deploy the Cursor hook to developer endpoints. Add a hooks.json plus a forwarder script. Scope options: ~/.cursor/hooks.json (per user), <project>/.cursor/hooks.json (per repo, committable), or Enterprise-managed hooks pushed from the Cursor team dashboard / MDM for tamper-resistant fleet-wide rollout.
{
"version": 1,
"hooks": {
"beforeSubmitPrompt": [{ "command": ".cursor/hooks/forward-prompt.sh" }]
}
}#!/usr/bin/env bash payload="$(cat)" # beforeSubmitPrompt event on stdin gen_id="$(printf '%s' "$payload" | jq -r '.generation_id // empty')" curl -s -X POST "https://<ingress-host>/api/v1/webhooks/cursor" \ -H "Content-Type: application/json" \ -H "X-Webhook-Secret: <axia-webhook-secret>" \ -H "X-Idempotency-Key: ${gen_id}" \ --data "$payload" >/dev/null echo '{"continue": true}' # allow the prompt; leave failClosed off so errors never block
Payload & idempotency. The beforeSubmitPrompt event carries prompt (the text), user_email, conversation_id, generation_id, attachments[], workspace_roots[], hook_event_name. There is no timestamp (stamp on receipt) and no id/event_id field — pass generation_id as X-Idempotency-Key (as above) so the receiver dedupes per prompt; conversation_id + generation_id is the stable composite key.
Central config & enforcement via MDM. Deploy and enforce the hook fleet-wide with your MDM (and/or Cursor Enterprise managed hooks from the team dashboard) so it is applied consistently and developers cannot remove or bypass it. Ad-hoc per-repo or per-user hooks.json is fine for testing but is removable by developers. Capturing prompt text is user-input monitoring — handle as a consent/policy matter and align with Cursor Privacy Mode. Leave failClosed off so a forwarder error never blocks a developer.
Verify. Submit a prompt in Cursor; the forwarder POSTs and the receiver returns 202 Accepted {"status":"accepted","source":"cursor",…}; the event appears on enrichment.pending and is processed by enrichment-worker-webhook.
Cursor references: Hooks (beforeSubmitPrompt) · LLM Safety & Controls — hooks-based DLP.
10 Configuration reference
Service & port map
| Port | Service | Host | Role |
|---|---|---|---|
| 5173 | dashboard | platform | Frontend v1 (nginx :80) |
| 5174 | application-frontend | platform | Frontend v2 (nginx) |
| 8088 | application-backend | platform | FastAPI backend v2 (int. :8080) |
| 8003 | app-backend | platform | Legacy NestJS backend v1 (int. :3000) |
| 8080 | object-storage-service | platform | S3-fronting object storage API |
| 8001 | events-service | platform | Events storage API |
| 8005 | case-service | platform | Case/analysis persistence |
| 8008 | chat-service | platform | Intel chat (Bedrock agent) |
| 8010 | engagement-service | platform | Outbound ticketing (Jira/ADO) |
| 8011 | webhook-connector | platform | Webhook ingest → Kafka |
| 8004 | connector-api | platform | API ingestion connector |
| 9092 | kafka | platform | Broker (int. :29092, KRaft) |
| 6379 | redis | platform | Pub/sub + config-plane listener |
| 8000 | llm-service | ML | Bedrock LLM gateway / detection |
| 8090 | parser-service | ML | Content parser |
| 8829 | rapidocr | ML | OCR backend (internal) |
| 27017 | DocumentDB | managed | MongoDB-compatible datastore |
Container images
Public: confluentinc/cp-kafka:8.1.1, ghcr.io/kafbat/kafka-ui:v1.4.2, redis:7.4-alpine.
Axia (ECR): object-storage-service, event-analyzer, enrichment-worker, connector, webhook, case-service, chat-service, events-service, engagement-service, application-backend, application-frontend, app-backend, dashboard; and on the ML host llm-service, parser-service, parser-rapidocr, data-classification. Registry pattern <AXIA_ECR_REGISTRY>/<service>:<tag> (exact registry provided by Axia) — tags pinned per release (e.g. v1.4.0 core / v1.0.0 app-layer); un-pinned entries default to latest.
Key environment variables & secrets
Config uses a namespaced convention: SERVICES__ENV__<SERVICE>__<VAR> (non-secret) and SERVICES__SECRETS__<SERVICE>__<VAR> (secret).
| Category | Variables |
|---|---|
| AWS / registry | ACCOUNT_ID, AWS_REGION, ECR_REGISTRY |
| Database | MONGODB_URI / per-service SERVICES__SECRETS__{APP_BACKEND_SERVICE,EVENTS_SERVICE,CHAT_SERVICE,INGESTION_SERVICE}__(MONGODB_URI|DB_URI) |
| Object storage | SERVICES__SECRETS__OBJECT_STORAGE_SERVICE__AWS_ACCESS_KEY_ID / __AWS_SECRET_ACCESS_KEY, ...__URL, ...__REGION, DATA_BUCKET, AXIA_BUCKET |
| Auth | JWT_SECRET (required for application-backend), AUTH_COOKIE_SECURE |
| LLM | LLM_MODEL (Bedrock model id), PROMPT_PATH, AWS_DEFAULT_REGION |
| Kafka | KAFKA_BOOTSTRAP_SERVERS, KAFKA_SECURITY_PROTOCOL, KAFKA_ADVERTISED_HOST |
| Cross-host | PARSER_SERVICE_URL, LLM_SERVICE_URL |
| Outlook / 365 | MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, OUTLOOK_ORG_DOMAIN |
| Purview DLP (§9.1) | MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, PURVIEW_START_DATE; defaults MICROSOFT_GRAPH_BASE_URL, PURVIEW_PENDING_QUEUE_NAME=dlp.events |
| Webhook — Cursor (§9.2) | WEBHOOK_ENRICHMENT_SOURCES (cursor:<secret>;…), WEBHOOK_ENRICHMENT_TOPIC, WEBHOOK_DLP_SOURCES, WEBHOOK_DLP_TOPIC, WEBHOOK_BATCH_SOURCES, WEBHOOK_BATCH_TOPIC |
| SMTP (optional) | SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM_ADDRESS/FROM_NAME |
In production the DocumentDB password comes from Secrets Manager and the S3 credentials from SSM Parameter Store — both retrieved at deploy time by the config generator, never stored in the repo.
11 Operations
- Connect to a host:
aws ssm start-session --target <instance-id> --region <region>(orterraform output ssm_command). Hosts are in private subnets — no SSH. - DocumentDB shell from your laptop: use
documentdb_ssm_port_forward_commandto maplocalhost:27017, then connect with a TLS client using the RDS CA bundle (authMechanism=SCRAM-SHA-1). - Upgrade a service: update the image tag (via
.envon the host or the CISERVICES_ENV_VARS), then re-run the deploy (docker compose pull && up -d). - Logs:
docker compose logs -f <service>on the host; container logs also ship to CloudWatch (/platform/services,/ml). - Off-hours shutdown: the platform EC2 can be tagged
shut-down=trueto opt into an off-hours scheduler (optional cost control).
12 Known gotchas & caveats
These are the failure modes most likely to bite a first deployment.
- Two hosts required. The platform compose file does not include
llm-serviceor the parser — those live on the ML host. Both must run and be wired by private-IP URLs. - DocumentDB, not plain MongoDB. Connections require TLS + the RDS CA bundle at
/app/certs/global-bundle.pemandauthMechanism=SCRAM-SHA-1. URL-encode the password (scripts/encode-mongodb-password.py). - Bedrock region.
llm-serviceandchat-serviceneedAWS_DEFAULT_REGIONset, or they fail withNoRegionError. Verify the model id againstaws bedrock list-foundation-models. - The image registry points at Axia's ECR in the generated
deployment-config.jsonby design — that is where images are pulled from cross-account (§5), not your account. The bundled config generator anddeploy.share already path-fixed and correctly named. - Region consistency. Pin one region across the whole deployment — state bucket, VPC, DocumentDB, S3, and the deployment config — and set it before you apply.
- Cross-account ECR trust is Axia-side. The host role grants pull actions, but Axia must add your account to each repository policy.
- AMI / user_data drift. The EC2 resources set
ignore_changes = [ami, user_data], so Terraform will not replace a running host on AMI updates. Use targeted applies for non-EC2 changes. - Bootstrap variants differ. Prefer the CloudFormation bootstrap (creates the OIDC provider the IAM stacks depend on).
- CI deploy is Axia-managed. The GitHub Actions deploy path runs from Axia's org; the manual deploy path documented here is the supported route for self-hosted deployments.
- Purview alerts can lag. New/modified DLP policies take up to ~3 hours to start generating alerts, and app-only reads require
SecurityAlert.Read.Allwith admin consent — a missing consent looks like "authenticated but zero alerts" (§9.1). - Cursor has no cloud-side prompt events. User prompts are captured only via the local
beforeSubmitPrompthook deployed to each developer endpoint (configure and enforce it via MDM for a tamper-resistant fleet-wide rollout). The hook's forwarder script setsX-Webhook-Secretitself, so no HMAC shim is needed for this path (§9.2). Capturing prompt text is user-input monitoring — treat as a consent/policy matter.
13 Bundle contents map
Everything the phases above reference, and where it lives inside axia-deploy-<version>/:
| Topic | Path (in the bundle) |
|---|---|
| Bootstrap CloudFormation | terraform/init/{terraform-backend.yaml,github-actions-role.yaml} |
| Network stack | terraform/vpc/ |
| Core infra stack (EC2 / DocumentDB / S3 / IAM) | terraform/platform/ |
| Host bootstrap script | terraform/platform/scripts/user_data.sh |
| Example env config | terraform/platform/{example.backend.hcl,example.tfvars} |
| Deploy scripts | terraform/scripts/{generate-deployment-config.sh,deploy.sh,ECR-cross-account-trust.sh} |
| Client config template | client-config.env.template |
| Platform host compose | compose/platform/docker-compose.yml |
| ML host compose | compose/ml/docker-compose.yml |
Axia-internal references (not shipped): the apps-ci/tenants Terraform stacks, the Terraform source-of-truth map, the CI deploy workflow (platform-deploy.yml), and the internal deployment-strategy / ports docs.