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:
- No
s3:PutObject— EdgeGate never writes to your bucket. - No
s3:ListBucket— EdgeGate cannot enumerate your model inventory. - 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 toPOST /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
¶m_WorkspaceExternalId=<WORKSPACE_EXTERNAL_ID>
¶m_EdgeGateAccountId=<EDGEGATE_ACCOUNT_ID>
¶m_CustomerBucket=<YOUR_BUCKET>
¶m_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-terraformis 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:
- EdgeGate audit log —
GET /v1/workspaces/{ws}/byo-storage/auditreturns everyassume_role,head_object,get_object, andverify_probeevent with request ID, role ARN, S3 key, bytes read, worker hostname, outcome, and error code. - 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 code | Class | Customer-facing meaning | Remediation |
|---|---|---|---|
BYO_ASSUME_ROLE_FAILED | User-actionable | STS 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_FOUND | Terminal | The 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_DENIED | User-actionable | The 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_DENIED | User-actionable | The 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_GONE | Terminal | The 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_MISMATCH | Terminal | The 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_TIMEOUT | Transient (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_LIMITED | Transient (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_MISMATCH | User-actionable | The 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_GRANT | Terminal | The 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