skybridge

module
v0.0.15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: Apache-2.0

README

Skybridge

Skybridge is a small Go data plane for governed native database access. An egress-only agent sits in front of your database, speaks the native client protocols (psql, mysql, mongosh, app drivers), and masks PII at the source — so raw rows never leave your network.

  • Stdlib-only wire-proxy core — manual protocol parsing + masking, no third-party deps.
  • Content-aware masking — pluggable remote masker + a column overlay you define.
  • Anything an engine can't parse is forwarded unmasked, never corrupted.
  • One edge binary (skybridge-edge) also dials home for live read-only AWS reads, so everything that must run inside your network is a single install.

Jump to: How it works · Quick start · Configure · The skybridge-edge binary · Deploying on AWS · Layout

How it works

flowchart LR
    subgraph clients["Native DB clients"]
        c1["psql / mysql / mongosh"]
    end

    subgraph net["Your network (egress-only)"]
        agent["Skybridge agent<br/>masks PII here"]
        edge["Skybridge edge<br/>call-home + AWS reads"]
        db[(Database)]
        aws["AWS account<br/>(read-only)"]
        agent --> db
        edge --> aws
    end

    subgraph saas["Control plane (SaaS)"]
        gw["Skybridge gateway"]
        cg["Connector Gateway"]
    end

    c1 -->|"listener mode"| agent
    c1 -->|"tunnel mode"| gw
    gw -. "egress tunnel<br/>(masked bytes only)" .-> agent
    cg -. "egress call-home<br/>(read-only tool calls)" .-> edge

Skybridge ships three deployment shapes; all of them keep the customer side egress-only (it dials out, nothing dials in):

  • Listener — native clients connect straight to the agent. Simplest setup.
  • Tunnel — the agent dials out to a gateway; clients connect to the gateway, which relays already-masked bytes over the tunnel. Masking still happens at the agent.
  • Edgeskybridge-edge dials out to the SaaS Connector Gateway and runs dispatched read-only tool calls locally — chiefly live AWS reads against your account — and can co-host the wire proxy in the same process. One install for everything that must run inside your network. See The skybridge-edge binary below.

Quick start

Put the agent in front of your database and point a native client at it. Pick one:

Run with Go (needs Go ≥ 1.26, no build step):

SKYBRIDGE_UPSTREAM=db.internal:5432 \
SKYBRIDGE_PII_OVERLAY='{"email":"[redacted]","ssn":"[redacted]"}' \
go run ./cmd/skybridge-agent

Run with Docker:

cd deploy
SKYBRIDGE_UPSTREAM=db.internal:5432 docker compose up

Then connect a native client through the agent (listening on :15432 by default):

psql "postgres://user:pass@localhost:15432/appdb"

That's it — result rows are masked before they reach the client. With no mask config the agent is a transparent, governed passthrough.

Configure

Set these as environment variables (full list in internal/config/config.go):

Variable Default What it does
SKYBRIDGE_UPSTREAM upstream database host:port (required)
SKYBRIDGE_DB_TYPE postgres postgres, mysql, or mongodb
SKYBRIDGE_LISTEN :15432 / :13306 / :27018 local address clients connect to
SKYBRIDGE_PII_OVERLAY JSON { "column": "[redacted]" } map you define (static)
SKYBRIDGE_PII_OVERLAY_URL control-plane endpoint to fetch the org's projected overlay (GET /api/v1/data-studio/studio/native-access/pii-overlay); enables dynamic, hot-swapped masking
SKYBRIDGE_PII_OVERLAY_TOKEN SKYBRIDGE_TOKEN bearer token for the overlay fetch
SKYBRIDGE_PII_OVERLAY_POLL_SECONDS 60 overlay refresh interval (min 15s; -1 = fetch once at startup)
SKYBRIDGE_MASK_ANALYZE_URL enable content masking: any POST /analyze service
SKYBRIDGE_MASK_ANONYMIZE_URL …paired POST /anonymize service
SKYBRIDGE_INJECT_CREDENTIALS false enable credential handoff (clients present a curlix session token, not a DB password)
SKYBRIDGE_CREDENTIAL_EXCHANGE_URL control-plane endpoint that swaps a session token for an upstream credential (POST /api/v1/data-studio/studio/native-access/proxy-exchange)
SKYBRIDGE_CREDENTIAL_EXCHANGE_TOKEN SKYBRIDGE_TOKEN bearer for the exchange call
SKYBRIDGE_CLIENT_TLS_CERT_FILE / _PEM server cert (Postgres) — enables terminating client TLS so the token isn't sent in cleartext
SKYBRIDGE_CLIENT_TLS_KEY_FILE / _PEM matching private key
SKYBRIDGE_CLIENT_TLS_SELF_SIGNED false dev: generate an ephemeral self-signed cert at startup (clients use sslmode=require)
SKYBRIDGE_UPSTREAM_TLS disable agent→database TLS (Postgres / MySQL / Mongo): disable | prefer | require | verify-ca | verify-full
SKYBRIDGE_UPSTREAM_TLS_CA_FILE / _PEM system roots trust roots used by verify-ca / verify-full (e.g. the RDS CA bundle)
SKYBRIDGE_UPSTREAM_TLS_SERVER_NAME dial host override the verified hostname / SNI sent to the upstream

Switch databases by changing SKYBRIDGE_DB_TYPE; everything else is identical.

Credential handoff (the client never holds a database password)

By default the agent forwards the client's authentication to the database verbatim, so the native client presents a real database credential. With credential injection enabled, the client instead presents an opaque curlix session token as its password; the agent terminates that login locally, exchanges the token with the control plane for a freshly-minted, short-lived upstream credential, and originates its own upstream authentication with it (Postgres: trust / cleartext / md5 / SCRAM-SHA-256; MySQL: mysql_native_password / caching_sha2_password). The client therefore never holds a credential the database would accept directly, and result rows are still masked inline.

SKYBRIDGE_DB_TYPE=postgres \
SKYBRIDGE_UPSTREAM=db.internal:5432 \
SKYBRIDGE_INJECT_CREDENTIALS=true \
SKYBRIDGE_TOKEN=<agent-service-bearer> \
SKYBRIDGE_CREDENTIAL_EXCHANGE_URL=https://app.example.com/api/v1/data-studio/studio/native-access/proxy-exchange \
go run ./cmd/skybridge-agent

The user first mints a session token from curlix (POST /api/v1/data-studio/studio/connections/{role_id}/proxy-session), then in pgAdmin/psql/DBeaver points the client at the Skybridge listener (not the database), uses any username, and pastes the session token as the password. Injection covers Postgres and MySQL; Mongo falls back to verbatim auth passthrough (logged at startup).

MySQL specifics. MySQL's default auth is challenge-response, so the token cannot be recovered from it. The agent therefore terminates client TLS and switches the client to the mysql_clear_password plugin to receive the token in cleartext over the encrypted link — so MySQL injection requires client TLS and the client must enable the cleartext plugin (e.g. mysql --enable-cleartext-plugin ..., or the equivalent checkbox in GUI tools). Upstream, the agent answers mysql_native_password or caching_sha2_password; the latter's first-connection "full authentication" sends the password over the wire, so it requires upstream TLS (SKYBRIDGE_UPSTREAM_TLS) — RSA-key full auth is not supported.

Encrypt the client link (so the token isn't sent in cleartext). The session token rides in the client's password, so terminate client TLS at the agent: provide a cert/key (or, for dev, a self-signed one) and connect the client with sslmode=require.

# dev: ephemeral self-signed cert; clients use sslmode=require (no chain verification)
SKYBRIDGE_DB_TYPE=postgres SKYBRIDGE_UPSTREAM=db.internal:5432 \
SKYBRIDGE_INJECT_CREDENTIALS=true SKYBRIDGE_TOKEN=… \
SKYBRIDGE_CREDENTIAL_EXCHANGE_URL=https://app.example.com/api/v1/data-studio/studio/native-access/proxy-exchange \
SKYBRIDGE_CLIENT_TLS_SELF_SIGNED=true \
go run ./cmd/skybridge-agent
# then: psql "host=localhost port=15432 user=me sslmode=require"  (password = the session token)

For production provide a real cert via SKYBRIDGE_CLIENT_TLS_CERT_FILE / SKYBRIDGE_CLIENT_TLS_KEY_FILE so clients can sslmode=verify-full. With client TLS off the agent logs a warning that the token is sent in the client's cleartext password — keep that link on a trusted hop.

Upstream TLS (encrypt the agent → database hop)

By default the agent speaks plaintext to the upstream over the trusted in-network path. Set SKYBRIDGE_UPSTREAM_TLS to negotiate TLS with the database after dialing (Postgres SSLRequest), mirroring libpq's sslmode:

Mode Behaviour
disable (default) plaintext to the upstream
prefer try TLS, fall back to plaintext if the server declines; no certificate verification
require TLS mandatory; no certificate verification (encrypt only)
verify-ca TLS mandatory; verify the certificate chain against the trust roots (skips hostname)
verify-full TLS mandatory; verify chain and hostname

This is also what unblocks rds_iam credential injection — the RDS IAM auth token is only accepted over a TLS connection. For RDS/Aurora, point the trust roots at the AWS RDS CA bundle:

SKYBRIDGE_DB_TYPE=postgres \
SKYBRIDGE_UPSTREAM=mydb.abc123.us-east-1.rds.amazonaws.com:5432 \
SKYBRIDGE_UPSTREAM_TLS=verify-full \
SKYBRIDGE_UPSTREAM_TLS_CA_FILE=/etc/ssl/rds/global-bundle.pem \
go run ./cmd/skybridge-agent

prefer/require encrypt the hop but do not authenticate the database; use verify-ca (handy when reaching the DB by IP) or verify-full to prove the server's identity.

Upstream TLS is negotiated for Postgres, MySQL and Mongo, but each protocol negotiates it differently:

  • PostgresSSLRequest before the protocol starts (transparent).
  • Mongo — TLS on connect (the server expects the handshake immediately). There is no in-band fallback, so prefer behaves like require for Mongo.
  • MySQL — TLS is negotiated inside the seq-numbered handshake, so the agent inserts its own SSLRequest packet and shifts the connection-phase sequence ids until auth completes. The upstream must advertise CLIENT_SSL; with require/verify-* a server that does not is a hard failure, while prefer falls back to a plaintext upstream. If the client itself speaks TLS to the agent, that connection drops to transparent passthrough (no masking, no upstream-TLS interception).
Dynamic PII overlay (from Administration → PII)

SKYBRIDGE_PII_OVERLAY is a static map. To keep native-client masking in sync with the column rules you define in Administration → PII, point the agent at the control plane instead:

SKYBRIDGE_ORG_ID=<org-uuid> \
SKYBRIDGE_TOKEN=<org-scoped-bearer> \
SKYBRIDGE_PII_OVERLAY_URL=https://app.example.com/api/v1/data-studio/studio/native-access/pii-overlay \
go run ./cmd/skybridge-agent

The agent fetches { "columns": { "<column>": "<token>" } } at startup and re-fetches every SKYBRIDGE_PII_OVERLAY_POLL_SECONDS, hot-swapping the overlay in place (no restart). Curlix projects the org's merged PII schema into per-category tokens (e.g. email → [email], ssn → [ssn]), excluding business identifiers / operational columns. A failed fetch leaves the last-known (or static SKYBRIDGE_PII_OVERLAY) rules intact. Note the wire overlay does full-cell replacement only; it cannot do the partial / value-shape redaction the in-process execute pipeline performs, so token counts differ between the two paths by design.

Layout

cmd/skybridge-agent     egress agent: listener OR tunnel mode
cmd/skybridge-gateway   relay gateway: agent endpoint + client listeners
cmd/skybridge-edge      unified edge: call-home transport + AWS reads + optional wire proxy
internal/wire           wire engines: postgres, mysql, mongo
internal/mask           masking pipeline: remote masker + column overlay
internal/tunnel         egress multiplexed transport
internal/gateway        agent registry + relay + optional session recording
internal/edge           edge tool dispatch: envelope, read-only AWS policy, executor
internal/edge/transport egress-only gRPC call-home client (Connect/serve/reconnect)
internal/certstore      persists issued mTLS identity (local disk, optionally mirrored to AWS
                         Secrets Manager) so redeployed tasks skip re-enrollment
internal/genpb          generated gRPC stubs (run `make gen` to refresh)
internal/config         SKYBRIDGE_* environment config
The skybridge-edge binary

skybridge-edge is the single thing a customer installs. It dials out (egress-only) to the SaaS Connector Gateway and serves dispatched single read-only tool calls locally — chiefly live AWS reads against the customer account (aws_readonly_cli, cloudwatch_logs_insights, cloudwatch_metrics) — and, when DB targets are configured, also runs the co-located wire proxy. The LLM agent loop and platform-coupled tools stay on the SaaS side.

SKYBRIDGE_EDGE_GATEWAY=gateway.example.com:8020 \
SKYBRIDGE_ORG_ID=org-123 SKYBRIDGE_EDGE_ID=edge-1 SKYBRIDGE_TOKEN=... \
SKYBRIDGE_AWS_REGION=us-east-1 \
go run ./cmd/skybridge-edge

Deploying on AWS? Use the ready-made CloudFormation template instead of setting these env vars by hand — see Deploying on AWS (CloudFormation) below.

Auth: bearer token vs. mTLS
Mode Set Behavior
Bearer (default) SKYBRIDGE_TOKEN Simple shared secret over TLS. Fine for a quick start.
mTLS (hardened) SKYBRIDGE_CA_BUNDLE_PEM/_FILE + SKYBRIDGE_ENROLLMENT_TOKEN The edge generates a keypair, calls Enroll once with the one-time token to get a signed client cert, then connects with mTLS. Preferred for production.

The issued cert is cached under SKYBRIDGE_TLS_DIR and reused on restart — no new token needed until it's actually close to expiry.

Keeping mTLS identity alive across redeploys

⚠️ On ECS/Fargate, a plain SKYBRIDGE_TLS_DIR cache does not survive a redeploy. Any task replacement (new image, new task definition, a CPU/memory bump — anything that spins up a fresh task) wipes that disk. Since SKYBRIDGE_ENROLLMENT_TOKEN is single-use, the new task can't just re-enroll: the token was already consumed by the task it replaced, and the deploy fails.

Fix: point the edge at an AWS Secrets Manager secret and it will keep its identity there too, so a replacement task picks up right where the old one left off — no new token required.

Identity Env var
Connector (edge → Connector Gateway) SKYBRIDGE_IDENTITY_SECRET_ARN
Query Studio (edge → Studio Gateway) SKYBRIDGE_STUDIO_IDENTITY_SECRET_ARN
Wire-mTLS (agent → relay gateway tunnel) SKYBRIDGE_WIRE_MTLS_IDENTITY_SECRET_ARN

Point one of these at a secret ARN the task's IAM role can GetSecretValue/PutSecretValue on (internal/certstore), and the edge mirrors its cert there on first enrollment, then loads from it on every subsequent start.

If you deploy via curlix-edge.yaml, this is already handled for you — the template provisions the secrets, the IAM permissions, and the env vars automatically. Nothing to configure.

Deploying on AWS (CloudFormation)

Most customers never touch the env vars above directly — they deploy integrations/curlix-connector/cloudformation/curlix-edge.yaml, which stands up an ECS Fargate task running this binary with everything wired: enrollment secret, persisted-identity secret(s), IAM roles, security group, log group.

aws cloudformation create-stack \
  --stack-name curlix-edge \
  --template-body file://integrations/curlix-connector/cloudformation/curlix-edge.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters file://parameters.json

Get parameters.json (VPC/subnets pre-filled, enrollment token included) from Curlix Administration → Connectors when you mint a new connector. To move to a newer image, update the stack's ConnectorImage parameter and redeploy — the mTLS identity above means this no longer requires a fresh enrollment token.

Docs

  • CONTRACT.md — the tunnel wire format and the gateway → control-plane HTTP session contract.
  • All SKYBRIDGE_* settings are documented inline in internal/config/config.go.

License

Apache-2.0 — see LICENSE.

Directories

Path Synopsis
cmd
skybridge-agent command
skybridge-edge command
internal
agent
Package agent runs the egress-side Skybridge data plane in either deployment mode:
Package agent runs the egress-side Skybridge data plane in either deployment mode:
certstore
Package certstore persists an enrolled edge's mTLS identity (ca.pem, client.crt, client.key) across process restarts.
Package certstore persists an enrolled edge's mTLS identity (ca.pem, client.crt, client.key) across process restarts.
config
Package config loads Skybridge configuration from the environment.
Package config loads Skybridge configuration from the environment.
edge
Package edge is the customer-side execution surface of the unified Skybridge edge binary.
Package edge is the customer-side execution surface of the unified Skybridge edge binary.
edge/awsexec
Package awsexec runs the live, read-only AWS tools at the customer edge.
Package awsexec runs the live, read-only AWS tools at the customer edge.
edge/transport
Package transport is the edge's egress-only call-home client.
Package transport is the edge's egress-only call-home client.
gateway
Package gateway is the relay-side Skybridge control plane.
Package gateway is the relay-side Skybridge control plane.
mask
Package mask applies PII redaction to result rows before they leave the egress network.
Package mask applies PII redaction to result rows before they leave the egress network.
tunnel
Package tunnel is the egress-only data-plane transport between a Skybridge agent and the relay gateway.
Package tunnel is the egress-only data-plane transport between a Skybridge agent and the relay gateway.
wire
Package wire holds the native database wire-protocol engines.
Package wire holds the native database wire-protocol engines.
wire/mongo
Package mongo implements the MongoDB wire protocol (OP_MSG / BSON) as a masking proxy.
Package mongo implements the MongoDB wire protocol (OP_MSG / BSON) as a masking proxy.
wire/mysql
Credential handoff (design "skybridge-go-wire-proxy" §7 phase 3) for MySQL.
Credential handoff (design "skybridge-go-wire-proxy" §7 phase 3) for MySQL.
wire/postgres
Credential handoff (design "skybridge-go-wire-proxy" §7 phase 3) for Postgres.
Credential handoff (design "skybridge-go-wire-proxy" §7 phase 3) for Postgres.
wiremtls
Package wiremtls implements mTLS identity for the Skybridge wire gateway↔agent tunnel (Phase 2, docs/design/identity-aware-network-access.md).
Package wiremtls implements mTLS identity for the Skybridge wire gateway↔agent tunnel (Phase 2, docs/design/identity-aware-network-access.md).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL