Farrier

Enterprise deployment

The reference deployments in deploy/ run Farrier the way a security review expects: the engine has no public address, every credential comes from the platform's secret manager through a workload identity, the master key is wrapped by a cloud KMS key, and data at rest is encrypted with customer-managed keys.

WhereModuleEngineWebDatabaseObjectsSecrets and keysEdge
AWSdeploy/terraform/awsECS Fargate ×2, internal ALBECS Fargate, internal ALBRDS PostgreSQL (Multi-AZ, CMK)S3 (SSE-KMS)Secrets Manager, KMS CMKCloudFront (VPC origin), optional WAF
Google Clouddeploy/terraform/gcpCloud Run, internal ingress, min 1Cloud Run, load-balancer ingressCloud SQL (private IP, HA)GCS (CMEK)Secret Manager, Cloud KMSExternal ALB, Cloud CDN, Cloud Armor
Azuredeploy/terraform/azureContainer Apps, environment-only ingress ×2Container AppsPostgreSQL Flexible Server (private access, HA)Blob storage (CMK)Key Vault (RBAC)Front Door Standard
On-prem, any VMdeploy/compose/production.ymlCompose ×2Compose, loopbackPostgreSQL 16MinIO or any S3Docker secrets, optional Vaultyour reverse proxy
Kubernetesdeploy/kubernetesStatefulSetDeploymentyour PostgreSQLyour bucketExternal Secrets + IRSA / Workload Identityyour ingress

Each Terraform module has a README.md with inputs, outputs, apply steps, a bring-your-own-network section, cost notes and teardown, and an examples/minimal.

Why the engine is never public

The engine holds the keys to your fleet: it can sign certificate requests, push certificates to load balancers and firewalls, and read device credentials. Only the dashboard needs to talk to it, so every reference deployment keeps it off the internet:

  • AWS: the engine sits behind an internal ALB whose security group admits only the web tasks. Even the web ALB is internal; CloudFront reaches it through a VPC origin.
  • Google Cloud: the engine's Cloud Run ingress is internal. The web service reaches it through Direct VPC egress.
  • Azure: the engine's Container Apps ingress is limited to its environment.
  • Compose: the engine is on an internal Docker network with no published port.

Its outbound path, to ACME CAs, DNS APIs and your devices' management interfaces, goes through your VPC: NAT for the internet, and VPN, Direct Connect, Interconnect or ExpressRoute to the devices.

AWS

Internet ─HTTPS─> CloudFront ─VPC origin─> internal ALB ─> web (Fargate)
                  caches /_next/static/*, /assets/*            │ FARRIER_API_URL
                                          internal ALB ────────┘ (web security group only)
                                               │
                                          engine ×2 ─> RDS · S3 · Secrets Manager · KMS · devices
WhatWhereWhy
Master keySecrets Manager <name>-runtime/master-key, wrapped by the KMS CMKPlaintext never touches Terraform state; unwrapping needs the engine's role
API tokens, session secretSecrets Manager <name>-runtime/*Injected at task start by a per-service execution role
Device credentialsSecrets Manager farrier/*The engine's role can read and write only this prefix
Database passwordGenerated and rotated by RDS in Secrets ManagerNever in state
Change packs, backups, audit exportsS3, SSE-KMS, versioned, TLS-only, Block Public Access; only the engine task role (and storage_admin_principal_arns) may write or delete objectsChange packs expire after 30 days, old versions after 90; a restored backup is trusted, so nobody else may write one
CacheCloudFront, static paths onlyDashboard, /api/* and /auth/* are never cached and get cookies and headers

Google Cloud

Internet ─HTTPS─> external ALB + Cloud Armor ─┬─ static paths ─> backend with Cloud CDN ─┐
                                               └─ everything else ─> backend, no CDN ──────┴─> Cloud Run web
Cloud Run web ─VPC─> Cloud Run engine (internal) ─VPC─> Cloud SQL private IP · Cloud NAT · devices
WhatWhere
Master keySecret Manager <name>-runtime-master-key, wrapped by Cloud KMS master-wrap
Device credentialsSecret Manager, names under farrier-; the engine's service account can read and add versions there only, by IAM condition
ObjectsGCS with uniform access, public access prevention, CMEK, versioning and lifecycle; the engine's service account holds the only bucket-level role
IdentityOne service account per service; no keys

Cloud Run cannot pull from ghcr.io, so the module creates an Artifact Registry remote repository that proxies it. On GKE, use the Kubernetes manifests with Workload Identity and keep the data tier from the module.

Azure

Internet ─HTTPS─> Front Door Standard ─(static paths cached)─> web app
Container Apps environment (VNet): web app ─> engine app (environment-only ingress)
engine ─> PostgreSQL (delegated subnet) · Blob and Key Vault (private endpoints) · devices
WhatWhere
Master keyKey Vault secret master-key, wrapped by Key Vault key master-wrap
App secretsRuntime Key Vault; each managed identity may read only its own secrets
Device credentialsA separate store Key Vault (FARRIER_SECRET_AZURE_VAULT_URL), where the engine is Secrets Officer
ObjectsStorage account: HTTPS only, TLS 1.2, no public or shared-key access, private endpoint, CMK, versioning, lifecycle; only the engine identity has a data role
EdgeThe web app's ingress admits only AzureFrontDoor.Backend addresses, and the app refuses requests without this profile's X-Azure-FDID

On-prem

deploy/compose/production.yml runs two engine replicas, the web app, PostgreSQL 16 and MinIO on one host, with Docker secrets instead of environment literals, read-only root filesystems, all capabilities dropped and no-new-privileges. Keep device credentials in HashiCorp Vault with FARRIER_SECRET_BACKEND=hashicorp. See deploy/compose/README.md.

Configuration contract

Every deployment configures the same variables.

Engine

VariableValue
FARRIER_DATABASE_URLpostgresql://farrier@db.internal:5432/farrier?sslmode=verify-full&sslrootcert=/path/ca.pem. No password in the URL.
FARRIER_DATABASE_PASSWORDenv:NAME, file:/path or the literal value
FARRIER_MASTER_KEYThe master key, or the KMS-wrapped master key when FARRIER_MASTER_KEY_KMS is set. In the cloud it always comes from the secret manager, never from a plain variable in Terraform.
FARRIER_MASTER_KEY_KMSaws-kms:arn:aws:kms:…:key/…, gcp-kms:projects/…/locations/…/keyRings/…/cryptoKeys/… or azure-kv:https://<vault>.vault.azure.net/keys/<name>. The engine unwraps the master key with it at boot.
FARRIER_API_TOKEN, FARRIER_API_READ_TOKENAdmin and read-only API tokens
FARRIER_SECRET_BACKENDlocal (default), aws, gcp, azure or hashicorp
FARRIER_SECRET_AWS_REGION, FARRIER_SECRET_AWS_PREFIX, FARRIER_SECRET_AWS_KMS_KEYRegion, name prefix (default farrier/), optional CMK for new secrets
FARRIER_SECRET_GCP_PROJECT, FARRIER_SECRET_GCP_PREFIXProject holding the secrets, name prefix (default farrier-)
FARRIER_SECRET_AZURE_VAULT_URL, FARRIER_SECRET_AZURE_PREFIXhttps://<vault>.vault.azure.net/, name prefix (default farrier-)
FARRIER_SECRET_VAULT_ADDR, FARRIER_SECRET_VAULT_MOUNT, FARRIER_SECRET_VAULT_PREFIXVault or OpenBao address, KV v2 mount (default secret), path prefix (default farrier/)
FARRIER_SECRET_VAULT_NAMESPACE, FARRIER_SECRET_VAULT_CACERTVault Enterprise namespace, CA bundle for the Vault server
FARRIER_SECRET_VAULT_TOKEN_FILEToken file (e.g. a Vault Agent sink), re-read on every use; else VAULT_TOKEN
FARRIER_SECRET_VAULT_ROLE, FARRIER_SECRET_VAULT_AUTH_MOUNT, FARRIER_SECRET_VAULT_K8S_TOKEN_FILEKubernetes auth: role, auth mount (default kubernetes), service-account token (default /var/run/secrets/kubernetes.io/serviceaccount/token)
FARRIER_SECRET_EXTERNAL_PREFIXESComma-separated prefixes that aws-sm:, gcp-sm:, azure-kv: and hcvault: references must start with (default: the backend's own location and prefix only)
FARRIER_STORAGE_URLs3://bucket/prefix, gs://bucket/prefix, azblob://account/container/prefix or file:///path. Default: objects/ in the data directory. Holds change packs, chains, backups and audit exports
FARRIER_STORAGE_S3_ENDPOINT, FARRIER_STORAGE_S3_REGIONMinIO or another S3-compatible endpoint (path-style), its region (default AWS_REGION, else us-east-1)
FARRIER_STORAGE_KMS_KEYKMS key for S3 server-side encryption
FARRIER_STORAGE_AZBLOB_ENDPOINTBlob endpoint override (default https://<account>.blob.core.windows.net)
FARRIER_INSTANCE_IDCloudEvent source id (farrier://<id>); default derived from the master key

Cloud backends always authenticate with the platform identity: an ECS task role, a Cloud Run or GKE service account through Workload Identity, an Azure managed identity, or Vault's Kubernetes auth. Never give the engine static cloud keys.

Web app

VariableValue
FARRIER_API_URLThe engine's private URL
FARRIER_API_TOKEN, FARRIER_API_READ_TOKENSame values as the engine
FARRIER_OIDC_ISSUER, FARRIER_OIDC_CLIENT_ID, FARRIER_OIDC_CLIENT_SECRET, FARRIER_SESSION_SECRETSingle sign-on (guide)
FARRIER_OIDC_REDIRECT_URIhttps://<dashboard host>/auth/callback, exactly as registered with the identity provider. Required in production; the modules and the Helm chart set it from the site URL
FARRIER_TRUSTED_PROXY_HOPSX-Forwarded-For entries your own proxies append (default 1). The modules set 2: CloudFront → ALB, Front Door → Container Apps ingress, and Google's load balancer, which appends two
FARRIER_REQUIRE_HEADERName:value a request must carry, else it is refused. The Azure module sets X-Azure-FDID:<Front Door profile GUID>. Every path is checked except /api/health, which platform probes call without the header; a malformed value refuses all requests
FARRIER_DASHBOARD_PASSWORDBasic-auth fallback when SSO is off
NEXT_PUBLIC_SITE_URLThe dashboard's public URL

Deploy with a module

cd deploy/terraform/aws/examples/minimal   # or gcp, azure
terraform init
terraform apply                            # infrastructure and empty secrets
# wrap the master key with the module's KMS key and store it (see the module README)
terraform apply -var secrets_seeded=true   # start the services

The two-step apply keeps the master key out of Terraform: you generate it, wrap it with the module's key, and store only the ciphertext. Generated API tokens and the session secret do pass through state, so keep state in an encrypted, access-controlled backend.

The key outlives the backups

Sealed private keys and database backups are useless without the master key, and the wrapped master key is useless without the KMS key. Keep both for at least as long as your backups, and test a restore. Backups are signed with a key derived from the master key; farrier restore refuses unsigned ones (made before signing) unless you pass --allow-unsigned. Even then, rows that set connector commands need FARRIER_ALLOW_API_COMMANDS=1.

After deploying, work through the hosting checklist, and see Databases and HA for replicas and migrating from SQLite.