Mortar — Bring Your Own Cloud (BYOC)

Status: design doc; first customer pilot planned. Aliyun + AWS only at first; GCP / Azure added when a paying customer asks for them.

BYOC lets enterprises run Mortar’s data plane in their own cloud account, while Mortar’s control plane orchestrates from our account. Mortar never touches customer data; we only issue credentials, push config, and read aggregate usage metrics.

This unblocks deployments at Fortune 500 / regulated industries (banks, hospitals, government) where data leaving the customer’s VPC is a non-starter.


TL;DR

  • Data plane (the customer’s data) lives entirely in the customer’s AWS / Aliyun account: RDS Postgres, OSS / S3, Redis / ElastiCache, FC / Lambda, SLS / CloudWatch logs.
  • Control plane (mortar CLI + the orchestrator) runs on Mortar’s account. Issues credentials, pushes config, reads metadata. Has NO access to customer rows. No web admin UI — provisioning + ongoing ops happen through CLI commands (mortar byoc init, mortar byoc rotate-role, etc.).
  • IAM cross-account role — customer creates a mortar-control-plane IAM role that Mortar’s account can assume. Scoped to: manage RDS / OSS / FC / Lambda resources. NOT scoped to: read row data, decrypt KMS keys, modify IAM.
  • Lower margin but unlocks contracts that require data residency in the customer’s account.

What lives where

┌─────────────────────────────────┐    ┌─────────────────────────────────┐
│  Mortar's AWS account           │    │  Customer's AWS account         │
│                                 │    │                                 │
│  ┌───────────────────────────┐  │    │  ┌───────────────────────────┐  │
│  │  mortar CLI (operator)    │  │    │  │  RDS Postgres (data)       │  │
│  │  Mortar orchestrator      │ ─┼────┼─▶│  S3 buckets (data)         │  │
│  │  Mortar billing service   │  │ as │  │  ElastiCache (cache)        │  │
│  │  Audit aggregator         │  │sume│  │  Lambda (compute)           │  │
│  └───────────────────────────┘  │role│  │  CloudWatch Logs            │  │
│                                 │    │  │  Mortar binary (data plane) │  │
│  ┌───────────────────────────┐  │    │  └───────────────────────────┘  │
│  │  Aggregated usage metrics │ ◀┼────┼─ (CloudWatch metric streams)    │
│  └───────────────────────────┘  │    │                                 │
└─────────────────────────────────┘    └─────────────────────────────────┘

Customer account (data plane)

  • RDS PostgreSQL — Mortar’s primary DB. Customer chooses size, storage class, encryption settings.
  • S3 buckets — file storage. Customer KMS keys.
  • ElastiCache (Redis) — cache + rate-limit counters.
  • Lambda functions — user-deployed compute. Customer’s IAM execution role.
  • CloudWatch Logs — function output + audit-event stream.
  • EC2 / EKS — runs the Mortar binary itself (auto-deployed by the orchestrator).

Mortar account (control plane)

  • mortar CLI + control-plane API — the operator surface + tenant management. No customer data; only metadata (which customer, what tier, what region).
  • Orchestrator — issues commands to the data plane through the cross-account role. Stateless; redeploys without touching customer state.
  • Billing — sums usage metrics, generates invoices.
  • Audit aggregator — pulls aggregate metric streams (per-customer request counts, ledger entries, etc.); never raw rows.

IAM setup

Customer creates a cross-account IAM role in their AWS account:

# mortar-control-plane.tf
resource "aws_iam_role" "mortar_control_plane" {
  name = "mortar-control-plane"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = { AWS = "arn:aws:iam::<MORTAR_ACCOUNT_ID>:root" }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = { "sts:ExternalId" = "<CUSTOMER_TENANT_ID>" }
      }
    }]
  })
}

resource "aws_iam_role_policy" "mortar_control_plane" {
  role = aws_iam_role.mortar_control_plane.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # Provision + tear down Mortar's data-plane resources.
      { Effect = "Allow", Action = "rds:*",         Resource = "*" },
      { Effect = "Allow", Action = "s3:*",          Resource = "arn:aws:s3:::mortar-*" },
      { Effect = "Allow", Action = "elasticache:*", Resource = "*" },
      { Effect = "Allow", Action = "lambda:*",      Resource = "*" },
      { Effect = "Allow", Action = "logs:*",        Resource = "*" },
      # Provision the Mortar binary host.
      { Effect = "Allow", Action = "ec2:*",         Resource = "*" },
      { Effect = "Allow", Action = "ecs:*",         Resource = "*" },
      # Read aggregate metrics for billing.
      { Effect = "Allow", Action = "cloudwatch:GetMetricStatistics", Resource = "*" },
      { Effect = "Allow", Action = "cloudwatch:ListMetrics",         Resource = "*" },
    ]
  })
}

External ID is mandatory — prevents the confused-deputy attack where one Mortar customer’s tenant ID is used to assume role into a different customer’s account.

What we deliberately exclude from the policy:

  • kms:Decrypt — Mortar can’t decrypt customer KMS keys; data stays customer-readable only.
  • iam:* — Mortar can’t elevate its own privileges or create new cross-account roles.
  • rds:Modify*Tags / Add*Permission on actual database — we manage Postgres metadata but never run arbitrary SQL.

The Aliyun equivalent is RAM cross-account roles (AssumeRole); same shape, different syntax.


Bootstrap flow

  1. Customer signs up via mortar login --new + mortar byoc init --cloud=aws.
  2. CLI prompts for AWS account ID + prints External ID + Terraform module to a local dir (./mortar-byoc/).
  3. Customer applies the Terraform. IAM role created in their account.
  4. Customer runs mortar byoc bind --role-arn=<arn> to register the role.
  5. Orchestrator assumes the role + provisions:
    • VPC + security groups
    • RDS Postgres (single-AZ for dev, Multi-AZ for prod tier)
    • S3 buckets (mortar-files-<tenant>-<random>, etc.)
    • ElastiCache cluster
    • EC2 / ECS instance running the Mortar binary
  6. Orchestrator writes the binary’s MORTAR_* env-vars from the provisioned endpoints. Customer Mortar boots, registers with the control-plane API.
  7. Customer runs mortar byoc domain --add=<host>; CNAME points at the customer-account Mortar.

Tear down: customer revokes the IAM role. Orchestrator can no longer operate; customer’s existing infrastructure keeps running until they manually delete it. No “abandoned by Mortar” failure mode — the binary is statelessly configured + customer-owned.


Pricing impact

BYOC customers pay Mortar control-plane fees + their own AWS bill. No metered Cloud credits from Mortar — the customer’s cloud spend is the metered usage.

PlanPer monthIncludes
BYOC Standard¥4 999 / monthmortar CLI + control-plane API, orchestrator, support email, audit aggregation
BYOC Enterprise¥19 999 / month+ dedicated SRE channel, SLA 99.9%, multi-region orchestration
BYOC Sovereigncustom+ on-prem variant (orchestrator runs inside customer’s perimeter)

Lower margin than hosted Mortar Cloud (we don’t markup the cloud bill), but unlocks deals we’d otherwise lose entirely:

  • Banks — every domestic 国有 bank insists on data not leaving their VPC / their datacenter. BYOC + Sovereign tier closes those.
  • Hospitals — HIPAA / PIPL-medical equivalent require named control over data location.
  • Fortune 500 R&D — proprietary trade-secret data; legal won’t approve hosted Mortar at any price.
  • Government — 政企 customers, especially in regulated industries (electricity, telecoms), require sovereignty over the data path.

Cloud support matrix

CloudStatusNotes
AWSFull BYOC support; reference customer in pilot.
阿里云Cross-account RAM role pattern; targeting first Chinese BYOC pilot.
腾讯云plannedSame cross-account pattern as Aliyun.
GCPWaiting on first customer; Workload Identity Federation gap analysis.
AzureSame — waiting on demand.
Private cloud (vSphere)plannedSovereign tier customers; needs the orchestrator to run on-prem.

Pilot acceptance criteria

  • Cross-account role Terraform module published.
  • Orchestrator assumes role + provisions RDS / S3 / ElastiCache / Lambda end-to-end against a fixture customer account.
  • Bootstrap flow walks through mortar byoc initbinddomain without manual ops.
  • Billing pulls CloudWatch metrics + invoices the per-tenant total.
  • First paying BYOC pilot signed.