EnterpriseOnboarding guide

EdgeGate BYO Storage — Customer Onboarding Guide

Status: v1.0 GA (BYO Storage Enterprise tier) Target time: 30 minutes from "contract signed" to "first private-model run" Audience: Enterprise platform + security teams


Who this is for

EdgeGate's Bring-Your-Own (BYO) Storage is an Enterprise-tier feature for customers with NDA / IP-custody requirements: model bytes must never leave the customer's AWS account. Typical adopters: device-OEM ML teams, defence contractors, regulated-industry SaaS — any org where uploading proprietary model weights to a third-party SaaS triggers a legal review.

Free, Pro, Team keep using the managed-S3 flow — strong audit trail, faster setup, no IAM ask. BYO is not a Team-tier add-on; it requires an annual contract because the support runbook is meaningfully different.


Overview

The customer keeps the model in their own S3 bucket. EdgeGate's worker performs a short-lived sts:AssumeRole against a customer-owned IAM role at run time (15-min session), streams the model to its /tmp, submits the compile/profile/evaluate job to Qualcomm AI Hub, and deletes the local copy. No persistent EdgeGate-side custody of model bytes. Every AWS call is audited with the AWS request ID for 1:1 correlation against your CloudTrail.

       customer S3                       EdgeGate worker (us-east-1)
   ┌─────────────────┐                 ┌─────────────────────────┐
   │  acme-models/   │  AssumeRole     │ short-lived creds       │
   │  finetune.onnx  │ ◄────────────── │ ExternalId=<UUID>       │
   │                 │                 │ DurationSeconds=900     │
   │                 │  GetObject      │                         │
   │                 │ ──────────────► │  /tmp/finetune.onnx     │
   └─────────────────┘                 │  → qai_hub.submit_*     │
                                       │  → rm /tmp/finetune.*   │
                                       └─────────────────────────┘

Three guarantees the security review cares about:

  1. No s3:PutObject — EdgeGate never writes to your bucket.
  2. No s3:ListBucket — EdgeGate cannot enumerate your model inventory.
  3. External ID in your trust policy blocks the confused-deputy attack class — guessing the role ARN is not enough to assume the role.

Phase 1 — AWS setup (~10 min)

You need to create one IAM role in your AWS account with a read-only S3 permission policy and a trust policy that allows EdgeGate's service principal to sts:AssumeRole, conditioned on the External ID EdgeGate gives you.

We publish the same trust policy in three formats. Pick the one your pipeline already supports — there is no "correct" answer.

Two values you need from EdgeGate:

  • WorkspaceExternalId — UUID, visible at Settings → BYO Storage in the dashboard, or returned from your first call to POST /grants.
  • EdgeGateAccountId — 12-digit AWS account ID, also on the same dashboard page. Stable across workspaces; not secret.

Option A — CloudFormation (one-click)

Click the Launch Stack URL below. It pre-fills the External ID and EdgeGate account ID; you provide your bucket name (and, optionally, your CMK ARN).

https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review
  ?templateURL=https://edgegate.frozo.ai/byo-storage/cloudformation.yaml
  &param_WorkspaceExternalId=<WORKSPACE_EXTERNAL_ID>
  &param_EdgeGateAccountId=<EDGEGATE_ACCOUNT_ID>
  &param_CustomerBucket=<YOUR_BUCKET>
  &param_CustomerKmsKeyArn=

The template itself, verbatim:

AWSTemplateFormatVersion: "2010-09-09"
Description: >
  EdgeGate BYO Storage — IAM role granting EdgeGate read-only access to a
  customer-owned S3 bucket. Trust is conditioned on a workspace-specific
  External ID. See docs/byo-storage-onboarding.md for setup walkthrough.

Parameters:
  EdgeGateAccountId:
    Type: String
    Description: AWS account ID hosting the EdgeGate principal customers trust.
    AllowedPattern: ^[0-9]{12}$
  EdgeGatePrincipalKind:
    Type: String
    Default: user
    AllowedValues: [user, role]
    Description: IAM identity type ("user" for single-hop, "role" for two-hop).
  EdgeGatePrincipalName:
    Type: String
    Default: edgegate-worker
    Description: IAM identity name EdgeGate calls AssumeRole as.
  WorkspaceExternalId:
    Type: String
    Description: UUID issued by EdgeGate when you register the bucket.
    AllowedPattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
  CustomerBucket:
    Type: String
    Description: Name of the S3 bucket EdgeGate should be allowed to read.
  CustomerKmsKeyArn:
    Type: String
    Default: ""
    Description: >
      Optional CMK ARN if the bucket uses customer-managed encryption.
      Leave blank for SSE-S3 / SSE-KMS with the AWS-managed key.

Conditions:
  HasCmk: !Not [!Equals [!Ref CustomerKmsKeyArn, ""]]

Resources:
  EdgeGateReadRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "edgegate-byo-read-${WorkspaceExternalId}"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${EdgeGateAccountId}:${EdgeGatePrincipalKind}/${EdgeGatePrincipalName}"
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref WorkspaceExternalId
      Policies:
        - PolicyName: EdgeGateReadModels
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:HeadObject
                Resource: !Sub "arn:aws:s3:::${CustomerBucket}/*"
              - !If
                - HasCmk
                - Effect: Allow
                  Action: kms:Decrypt
                  Resource: !Ref CustomerKmsKeyArn
                - !Ref AWS::NoValue

Outputs:
  RoleArn:
    Description: Paste this into edgegate_register_byo_bucket.
    Value: !GetAtt EdgeGateReadRole.Arn

Copy the RoleArn from the stack's Outputs tab — you'll paste it into EdgeGate in Phase 2.

Option B — Terraform

v1 status: the published module at github.com/frozo-ai/edgegate-byo-terraform is planned for v1.1 and not yet released. Use the inline HCL below until it ships — this is the exact resource set the module will emit.

variable "edgegate_account_id"   { type = string }
variable "workspace_external_id" { type = string }
variable "customer_bucket"       { type = string }
variable "customer_kms_key_arn"  { type = string; default = "" }

resource "aws_iam_role" "edgegate_byo_read" {
  name = "edgegate-byo-read-${var.workspace_external_id}"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::${var.edgegate_account_id}:user/edgegate-worker" }
      Action    = "sts:AssumeRole"
      Condition = { StringEquals = { "sts:ExternalId" = var.workspace_external_id } }
    }]
  })
}

resource "aws_iam_role_policy" "edgegate_byo_read" {
  role = aws_iam_role.edgegate_byo_read.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = concat(
      [{ Effect = "Allow", Action = ["s3:GetObject", "s3:HeadObject"],
         Resource = "arn:aws:s3:::${var.customer_bucket}/*" }],
      var.customer_kms_key_arn == "" ? [] :
      [{ Effect = "Allow", Action = ["kms:Decrypt"],
         Resource = var.customer_kms_key_arn }],
    )
  })
}

output "role_arn" { value = aws_iam_role.edgegate_byo_read.arn }

terraform apply, then copy the role_arn output.

Option C — Raw JSON

For security teams that want line-by-line review without a tool wrapper.

Trust policy (attach to the IAM role's "Trust relationships" tab):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowEdgeGateAssumeRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<EDGEGATE_ACCOUNT_ID>:user/edgegate-worker"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "<WORKSPACE_EXTERNAL_ID>"
        }
      }
    }
  ]
}

Permission policy — no CMK (SSE-S3 or SSE-KMS with the AWS-managed key):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EdgeGateReadModels",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:HeadObject"
      ],
      "Resource": "arn:aws:s3:::<CUSTOMER_BUCKET>/*"
    }
  ]
}

Permission policy — with CMK (SSE-KMS with a customer-managed key):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EdgeGateReadModels",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:HeadObject"
      ],
      "Resource": "arn:aws:s3:::<CUSTOMER_BUCKET>/*"
    },
    {
      "Sid": "EdgeGateDecryptCmk",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": "<CUSTOMER_KMS_KEY_ARN>"
    }
  ]
}

If you use a CMK, you also need to add EdgeGate's role as a key user in the KMS key policy. AWS UI: KMS → your key → Key policy → Add → Other AWS accounts, paste the role ARN you just created.


Phase 2 — EdgeGate registration (~3 min)

Option A — MCP tool

/edgegate-byo-storage

edgegate_register_byo_bucket {
  "workspace_id": "<your workspace UUID>",
  "role_arn":     "arn:aws:iam::<YOUR_ACCOUNT_ID>:role/edgegate-byo-read-<EXTERNAL_ID>",
  "bucket":       "acme-prod-models",
  "region":       "us-east-1",
  "kms_key_id":   null
}

The tool runs the readiness probe — sts:AssumeRole + HeadObject against a synthetic key __edgegate-readiness-probe__. Expected: 404 on the HeadObject (the key doesn't exist; we asked deliberately). That proves the trust chain end-to-end without touching any real models.

Option B — Dashboard

Settings → BYO Storage → Register bucket → paste role ARN + bucket + region → Verify & Save. Same probe, same outcome surface.

Option C — curl

curl -X POST \
  "https://edgegateapi.frozo.ai/v1/workspaces/${WORKSPACE_ID}/byo-storage/grants" \
  -H "Authorization: Bearer ${EDGEGATE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "role_arn":   "arn:aws:iam::123456789012:role/edgegate-byo-read-...",
    "bucket":     "acme-prod-models",
    "region":     "us-east-1",
    "kms_key_id": null
  }'

A 201 means probe passed; the response body includes external_id, last_verified_at, and status: "active". A 4xx body has a specific BYO_* error code and a one-line remediation hint — see the failure table at the end of this doc.

Re-verifying on demand

Customers commonly schedule a daily verify from their security monitoring to catch IAM drift before it bites a production run.

curl -X POST \
  "https://edgegateapi.frozo.ai/v1/workspaces/${WORKSPACE_ID}/byo-storage/grants/verify" \
  -H "Authorization: Bearer ${EDGEGATE_API_KEY}"

MCP equivalent: edgegate_check_byo_bucket.


Phase 3 — First artifact + first run (~5 min)

Once the grant is active, point EdgeGate at your model. EdgeGate calls HeadObject via the assumed role to capture size + ETag + S3 VersionId, stores only that metadata — never the bytes.

Register the artifact

edgegate_register_byo_artifact {
  "workspace_id":    "<workspace UUID>",
  "s3_uri":          "s3://acme-prod-models/finetune-v3.onnx",
  "expected_sha256": "9c1e1a...optional, recommended",
  "expected_size":   42997104
}

curl equivalent:

curl -X POST \
  "https://edgegateapi.frozo.ai/v1/workspaces/${WORKSPACE_ID}/artifacts/byo" \
  -H "Authorization: Bearer ${EDGEGATE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "s3_uri":          "s3://acme-prod-models/finetune-v3.onnx",
    "expected_sha256": "9c1e1a...",
    "expected_size":   42997104
  }'

The S3 URI's bucket must match the registered grant's bucket; otherwise you get a 400 with BYO_BUCKET_MISMATCH. The response body is a normal EdgeGate artifact record — artifact_id is interchangeable with managed artifacts everywhere downstream.

Create a pipeline and trigger a run

edgegate_create_pipeline {
  "workspace_id": "<workspace UUID>",
  "name":         "production-gate",
  "artifact_id":  "<artifact UUID from previous step>",
  "devices":      ["snapdragon-8-gen-3"],
  "gates":        { "inference_time_ms": 1.0, "peak_memory_mb": 150 }
}

edgegate_run_gate {
  "workspace_id": "<workspace UUID>",
  "pipeline_id":  "<pipeline UUID>"
}

The worker AssumeRoles, GetObjects your model to /tmp, submits to Qualcomm AI Hub, and deletes the local copy when the cell finishes (or errors). Two audit rows land in EdgeGate's audit log per cell: one for assume_role, one for get_object. Both carry the AWS request ID.

The signed evidence bundle includes a per-cell storage_provenance field set to byo-s3 along with the bucket name, so downstream reviewers can prove the model never custody-flipped to EdgeGate's infrastructure.


Security team Q&A cheat sheet

Print this section. Share it with your security review.

Q: Does EdgeGate store our model bytes anywhere? A: No. Bytes stay in your S3 bucket. The worker streams them into /tmp on a single host immediately before submission to Qualcomm AI Hub, then deletes. No EdgeGate-owned S3, no EdgeGate DB column holds your model.

Q: Can EdgeGate write to our bucket? A: No. The IAM permission policy grants only s3:GetObject and s3:HeadObject. CI (tests/unit/test_byo_storage_iam_templates.py) specifically blocks s3:PutObject, s3:ListBucket, and any s3:Delete* from ever being added by accident.

Q: Can EdgeGate enumerate the rest of our bucket? A: No. Without s3:ListBucket, EdgeGate can only read objects whose exact S3 URIs you registered through POST /artifacts/byo.

Q: How do we audit every read EdgeGate makes against our data? A: Two layers, joined by AWS request ID:

  1. EdgeGate audit log — GET /v1/workspaces/{ws}/byo-storage/audit returns every assume_role, head_object, get_object, and verify_probe event with request ID, role ARN, S3 key, bytes read, worker hostname, outcome, and error code.
  2. Your CloudTrail — AWS writes the same request ID into your account.

See the CloudTrail walkthrough below.

Q: What happens if we rotate the External ID? A: POST /grants/{id}/rotate-external-id issues a new UUID and runs both old + new for a 7-day overlap. The first successful verify with the new ID, or the 7-day expiry, purges the old one. After 7 days with no update, the grant flips to status='failed' and the next run errors with BYO_ASSUME_ROLE_FAILED.

Q: What's the SLA on cache invalidation after rotation? A: Immediate — the rotation endpoint calls ByoStorageService.invalidate_cache(role_arn) synchronously before returning. Next AWS call re-AssumeRoles with the current External ID. POST /grants/verify also invalidates without rotating.

Q: STS session duration? A: 15 minutes (AWS minimum), to bound the blast radius of a worker compromise. A multi-cell matrix run holds creds for at most 14 minutes (cache TTL) before re-AssumeRole.

Q: What region does EdgeGate connect from? A: us-east-1 for v1. Worker egress IPs are published on our security page; high-sensitivity customers can add aws:SourceIp to their bucket policy as documented hardening.

Q: What if our bucket uses a customer-managed KMS key? A: Use the "with CMK" permission policy (Option C above). Add EdgeGate's role as a key user in the KMS key policy. Pass the CMK ARN in kms_key_id at registration.

Q: Is the bucket-region setting load-bearing? A: Yes. If the registered region doesn't match the bucket's actual region, HeadObject returns BYO_REGION_MISMATCH naming both regions. Re-register with the correct region.

Q: Audit-log retention? A: 13 months in EdgeGate's DB (one fiscal year + buffer). JSON export to your own bucket is available on request; no auto-export.

Q: Can we test the flow with a dummy model first? A: We won't ship a test model into your bucket (would violate "never write"). Use any small ONNX you have — the path is identical.


CloudTrail correlation walkthrough

Every BYO audit row carries an AWS request ID. AWS writes the same request ID into your CloudTrail. Joining the two is one query each side.

Step 1 — Fetch EdgeGate's view

# All BYO events for an arbitrary 1-hour window
curl -s \
  "https://edgegateapi.frozo.ai/v1/workspaces/${WORKSPACE_ID}/byo-storage/audit?since=2026-06-06T14:00:00Z" \
  -H "Authorization: Bearer ${EDGEGATE_API_KEY}" \
  | jq '.entries[] | {ts, event_type, aws_request_id, role_arn, s3_key, outcome}'

Sample output:

{
  "ts": "2026-06-06T14:22:31Z",
  "event_type": "assume_role",
  "aws_request_id": "e1f4d5c0-...-aws-stsrequest",
  "role_arn": "arn:aws:iam::123456789012:role/edgegate-byo-read-...",
  "s3_key": null,
  "outcome": "success"
}
{
  "ts": "2026-06-06T14:22:32Z",
  "event_type": "get_object",
  "aws_request_id": "f8123abc-...-aws-s3request",
  "role_arn": "arn:aws:iam::123456789012:role/edgegate-byo-read-...",
  "s3_key": "finetune-v3.onnx",
  "outcome": "success"
}

MCP equivalent: edgegate_get_byo_audit { workspace_id, since: "..." }.

Step 2 — Find the same events in your CloudTrail

# AssumeRole side — search the CloudTrail "Event history" or run:
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
  --start-time 2026-06-06T14:00:00Z \
  --end-time   2026-06-06T15:00:00Z \
  | jq '.Events[] | select(.CloudTrailEvent
        | fromjson
        | .userIdentity.arn
        | contains("edgegate-worker"))'

# GetObject side — data events, requires data-event logging enabled on the
# bucket. Look up by AWS request ID.
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=acme-prod-models \
  --start-time 2026-06-06T14:00:00Z \
  --end-time   2026-06-06T15:00:00Z \
  | jq '.Events[] | select(.CloudTrailEvent | fromjson | .requestID == "f8123abc-...")'

Step 3 — Join

For each EdgeGate audit row, the AWS event with the matching requestID shows the assumed role, source IP, user agent, and exact API call. Every EdgeGate row should appear in your CloudTrail, and vice versa for any edgegate-worker-attributed event. A divergence is a security event — page our on-call and we'll triage same business day.


Failure mode reference

Every BYO failure surfaces with a specific BYO_* error code on the run record (run.error_code + run.error_detail). Customer-facing remediation is one line per code.

Error codeClassCustomer-facing meaningRemediation
BYO_ASSUME_ROLE_FAILEDUser-actionableSTS rejected the AssumeRole. Either the trust policy was changed, External ID drifted, or the role was deleted.Re-apply the trust policy from Phase 1. If you've rotated the External ID, confirm both old and new are valid in the overlap window. Then call POST /grants/verify.
BYO_OBJECT_NOT_FOUNDTerminalThe S3 object at s3://<bucket>/<key> no longer exists.Re-upload the model or register the new key with POST /artifacts/byo.
BYO_OBJECT_ACCESS_DENIEDUser-actionableThe role exists and is trusted, but the bucket / object policy denies GetObject.Re-apply the permission policy. Check for an explicit-deny in the bucket policy that overrides the role permission.
BYO_KMS_ACCESS_DENIEDUser-actionableThe object is encrypted with a CMK that doesn't allow the role to decrypt.Add the EdgeGate role as a key user in the CMK's key policy. Re-register with kms_key_id set.
BYO_BUCKET_GONETerminalThe S3 bucket itself was deleted, renamed, or moved.Re-create or re-register the bucket. Register a new grant — the bucket name is immutable on the existing one.
BYO_INTEGRITY_MISMATCHTerminalThe bytes EdgeGate streamed don't match the SHA-256 you registered.Re-register the artifact with the current SHA-256, or investigate why the object content changed unexpectedly. Versioned buckets prevent this class.
BYO_NETWORK_TIMEOUTTransient (retried 3×)EdgeGate's worker couldn't reach AWS within the timeout.None usually needed; the worker retries with 5s / 30s / 90s backoff. If the final attempt also times out, check AWS Health and EdgeGate status.
BYO_STS_RATE_LIMITEDTransient (retried 5×)STS throttled the AssumeRole calls (rare, typically a noisy-neighbor effect on your account).None usually needed; the worker retries with 5s / 10s / 30s / 60s / 120s backoff. If persistent, open a ticket with AWS for an STS quota increase.
BYO_REGION_MISMATCHUser-actionableThe bucket's actual region differs from the region you registered.Re-register the grant with the correct region. Buckets cannot move regions, so this is always a one-line fix.
BYO_NO_GRANTTerminalThe artifact references a byo_grant_id that no longer exists. Should not happen in practice — the FK is ON DELETE RESTRICT.Contact EdgeGate support.

The full coverage matrix mapping each code to its test is in tests/byo_storage_coverage.md (CI-gated).

Mid-run revocation

If your security team revokes the IAM trust during a multi-cell run, cells that already started succeed; subsequent cells fail with BYO_ASSUME_ROLE_FAILED. The run rollup names exactly which cells completed and which didn't — partial results are never silently published as a regression.


Further reading

  • Design spec: docs/superpowers/specs/2026-06-06-byo-storage-enterprise-design.md
  • Implementation plan: docs/superpowers/plans/2026-06-06-byo-storage-enterprise.md
  • Release notes: docs/release-notes-byo-storage-v1.md
  • Coverage matrix: tests/byo_storage_coverage.md
  • Internal staging notes (EdgeGate eng only): docs/internal/byo-storage-staging.md