Freestyle Docs

Freestyle / Docs

Outbound TLS

Connect VMs to APIs, databases, storage, and proxies.

Use outbound rules to connect your VM to services through Freestyle. Depending on the rule, Freestyle can supply credentials, transform HTTP requests, scope S3 access, or route connections through a proxy or to another VM.

Credential injection lets the VM authenticate without receiving the real API key or database password.

To publish a VM’s port to public clients, see Inbound TLS.

Reach A Domain With A Secret Injected

This rule lets a VM call api.openai.com. Freestyle adds the Authorization header before forwarding each request. Create the rule from your trusted application, where you keep the real API key.

import { Freestyle } from "freestyle";

const freestyle = new Freestyle();

await freestyle.tls.rules.create({
  action: "allow",
  domain: "api.openai.com",
  source: { vmId: "vm-123456" },
  destination: { public: true },
  transform: [{ headers: { authorization: "Bearer sk-…" } }],
});

The VM sends requests to the usual API URL without the real key. If a client SDK requires a key, give it a placeholder. The edge stores the configured header values encrypted and returns "***" when you read the rule; header names remain visible.

source selects the VM allowed to use the rule. Use source: { vpcId: "vpc-backend" } to allow every VM in a VPC. destination: { public: true } forwards to the domain’s public origin.

Freestyle adds an allowed path to the edge and configures the VM’s /etc/hosts to send requests for that exact domain there. This works even without a broad public-egress firewall rule.

For Git credentials, see Use Private Git Repositories in a Sandbox.

The installed entries can include both IPv4 and IPv6 addresses. The client must use an address reachable from its own network. Containers and clients that bypass the VM’s hosts file do not automatically follow those entries. See Docker TLS routing for container hostname mappings and an IPv6-enabled network. A client that dials the origin’s IP directly bypasses this steering and receives no injected headers; any direct connection still needs a firewall grant.

The client must trust the Freestyle CA installed in the VM when egress is configured; see client certificate trust. Clients that pin the origin’s certificate will reject the edge’s certificate. If you can configure the client to use a different hostname and trust the Freestyle CA, use an alias with a host destination. Host-alias rules currently need an existing allowed path to the edge; see the restricted-egress limitation. The direct-provider public: true rule above supplies an automatic edge grant.

// The guest dials vendor-alias.internal; the edge originates to the real host.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "vendor-alias.internal",
  source: { vmId: "vm-123456" },
  destination: { host: "api.vendor.com", port: 443 },
  transform: [{ headers: { authorization: "Bearer sk-…" } }],
});

Configure Client Certificate Trust

On Ubuntu, Freestyle installs its public CA at /usr/local/share/ca-certificates/freestyle-tls.crt and refreshes /etc/ssl/certs/ca-certificates.crt. This changes the VM’s system store; it does not modify every runtime or a Docker container’s filesystem. The same distinction applies after you install your own proxy or private CA.

Point each client at the bundle containing the required CA. For processes running directly in an Ubuntu VM:

ClientEnvironment setting
Python RequestsREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Python HTTPXSSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
Node.jsNODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt

Pass these variables through vm.exec({ command, env: { ... } }), your service configuration, or the application’s launch command. Restart existing clients after changing trust; Node reads its additional-CA setting at startup.

Requests normally uses certifi. Installing a CA in the system store, setting only SSL_CERT_FILE, or mounting a certificate file without configuring Requests is insufficient. For an explicit request, pass the bundle with verify:

response = requests.get(url, verify="/etc/ssl/certs/ca-certificates.crt", timeout=30)

For Session.send() with a prepared request, pass verify explicitly or merge the session’s environment settings. That flow does not automatically apply REQUESTS_CA_BUNDLE. Likewise, a custom HTTPX client with trust_env=False needs an explicit SSL configuration instead of relying on environment variables. See Node’s CA setting for clients that supply their own TLS ca option.

Inside Docker, the bundle must exist inside the container, and the variables must be set on its process. Use the container CA recipes. Registry pulls have a separate daemon trust configuration. Keep verification enabled; a working curl request in the VM does not prove that Requests or a container trusts the same CA.

Reach A Database With Credentials Injected

Connect to an external Postgres database without storing its password in the VM. Freestyle authenticates to the database with the credentials in the rule.

// vm-123456 may reach db.vendor.com, connecting as `app` with a password it
// was never given.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "db.vendor.com",
  source: { vmId: "vm-123456" },
  destination: { public: true },
  transform: [{ postgres: { username: "app", password: "…", database: "prod" } }],
});

protocol is inferred from the transform. In the guest, connect with any user and no password:

psql "postgresql://db.vendor.com/?sslmode=require"

The edge replaces user — and database when the transform names one — and completes the origin’s authentication, whether it asks for SCRAM-SHA-256, MD5, or a cleartext password. password is write-only: sealed at rest and read back as "***". The origin’s certificate is verified against the public root CAs on every connection.

A postgres transform belongs on an egress rule: a destination of public: true or a host. On a rule landing on your own VM it is refused.

A public origin with no port defaults to 5432.

Scope S3 Access Without Giving The VM Credentials

An s3 transform authorizes each request and signs it with AWS Signature V4 at our TLS edge. The VM never receives the configured credentials. Requests outside the bucket, key prefix, or operation allowlist return 403 before contacting S3.

await freestyle.tls.rules.create({
  action: "allow",
  domain: "my-bucket.s3.us-west-2.amazonaws.com",
  source: { vmId },
  destination: { public: true },
  transform: [{
    s3: {
      region: "us-west-2",
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        // Include when using temporary credentials; rotate before expiry.
        sessionToken: process.env.AWS_SESSION_TOKEN,
      },
      scope: {
        bucket: "my-bucket",
        prefix: `repos/${projectId}/`,
        operations: [
          "GetObject", "HeadObject", "PutObject", "DeleteObject",
          "ListObjects", "ListObjectsV2", "CreateMultipartUpload", "UploadPart", "ListParts",
          "CompleteMultipartUpload", "AbortMultipartUpload", "ListMultipartUploads",
        ],
      },
    },
  }],
});

protocol is inferred as http. Credentials are write-only: sealed at rest and returned as "***", including the access key ID and optional session token. Region, bucket, prefix, and operations remain visible. Rotate credentials with tls.rules.update, supplying the entire rule and new credentials; there is no automatic STS refresh.

The prefix is a literal UTF-8 object-key prefix, not a glob. A trailing / makes workspaces/vm-123/ a directory-like boundary; workspaces/vm-123 also matches workspaces/vm-123-other. An explicitly empty prefix permits all keys in the bucket. Both object and multipart-upload listings require an explicit prefix query parameter within the configured scope. An unscoped bucket listing is rejected rather than silently rewritten.

Each multipart operation is independently allowlisted and checked against the bucket and key. S3 manages upload IDs, stores parts, validates the completion manifest, and assembles the object. Grant AbortMultipartUpload so clients can clean up failed uploads, and ListParts to resume uploads. ListMultipartUploads permits discovering incomplete uploads only with a scoped listing prefix.

The rule supports virtual-hosted addressing (bucket.endpoint/key) and path-style addressing (endpoint/bucket/key). Use a concrete domain; for an alias, pin destination: { host: "s3.example.com", port: 443 }. The bucket is checked against the final origin and path, not the guest’s Host header.

The guest can send unsigned HTTP requests or requests signed with dummy credentials. The edge replaces authentication headers and signs the final upstream request. Query-signed/presigned URLs are rejected. Clients must trust the platform CA as described above. This rule controls access using its credential; other independently allowed network paths and credentials remain subject to the VM firewall and provider permissions.

Custom Endpoints And Shared Repositories

For Latitude-style path addressing, use domain: "s3.us-west-2.storage.sh", destination: { public: true }, and scope.bucket: "blendmux". Requests go to https://s3.us-west-2.storage.sh/blendmux/repos/<projectId>/<key>. Create a rule for each VM with the same repos/<projectId>/ scope; viewers can have only GetObject, HeadObject, ListObjects, and ListObjectsV2. A VPC source can share one rule when every member should have the same permissions.

For boto3-based clients in an Ubuntu VM, set placeholder credentials and the checksum fallback (the real credentials belong only in the TLS rule):

export AWS_ACCESS_KEY_ID=placeholder
export AWS_SECRET_ACCESS_KEY=placeholder
export AWS_DEFAULT_REGION=us-west-2
export AWS_ENDPOINT_URL_S3=https://s3.us-west-2.storage.sh
export AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required

Set s3.addressing_style = path in the AWS profile, or pass Config(s3={"addressing_style": "path"}) to boto3. Unset any inherited AWS_SESSION_TOKEN; the edge injects its configured token if one is needed. If-None-Match, If-Match, and Range are forwarded and signed. Provider responses such as 412 PreconditionFailed and 206 Partial Content pass through unchanged. Checksummed downloads using x-amz-checksum-mode work.

ListObjects grants ListObjects v1 and ListObjectsV2 grants ListObjectsV2. Clients such as boto3’s Bucket.objects.filter() list with v1.

Do not assume stock Git helpers satisfy the strict listing policy. In git-remote-s3 revision 60762e2, git-remote-s3 strips trailing slashes from the configured repo prefix and lists that bare prefix; its root listings must append / to fit a rule scoped to repos/<projectId>/. Widening the rule to the bare prefix would also admit similarly named sibling repos. These client adaptations and any custom lazy-mount implementation must be tested against their actual traffic before calling the entire stack compatible. The edge does not supply Git push hooks or history tracking.

Deleting a rule removes its grant after the existing edge lookup cache expires (up to five seconds); it does not cancel transfers already authorized and in flight. Granting DeleteObject authorizes deletion anywhere under the configured prefix, including history objects. Storage versioning and recovery policies are configured separately at the provider.

Supported Request Formats

Bodies stream over verified HTTPS using UNSIGNED-PAYLOAD, including multipart parts and completion XML. Policies requiring signed payload hashes are not supported. Standard checksum headers are forwarded. Configure SDKs to send ordinary request bodies with checksums in headers, rather than aws-chunked encoding or checksum trailers; those formats fail closed. Multipart uploads are supported independently of AWS chunked transfer signing.

CopyObject, UploadPartCopy, bulk deletion, object versions, ACLs, tagging, explicit encryption headers, bucket administration, and unknown query parameters or x-amz-* headers are rejected. Bucket default encryption still applies. Dot-segment and backslash paths are rejected to avoid ambiguous interpretations across providers. S3 must support SigV4 and unsigned payloads over HTTPS; provider-specific extensions require explicit support.

An s3 transform must be the rule’s only transform and cannot be combined with match. Its scope is an authorization boundary: it never falls through to unsigned forwarding. The dashboard displays S3 rules; create or update them through the SDK.

Match HTTP Egress Requests

An optional match beside transform selects which HTTP egress requests receive all of that rule’s transforms:

match: {
  method: ["POST", "PUT"],
  path: { exact: "/v1/responses" },
},
transform: [
  { headers: { authorization: `Bearer ${providerKey}` } },
  { jsonPatch: [{ op: "add", path: "/model", value: "approved-model" }] },
],
Field or behaviorMeaning
matchConditions apply to the entire transform list. Omitted fields impose no condition; {} matches every request.
method: string[]Any listed method matches; an empty array matches none. Method and path conditions must both match.
Method spellingCase-sensitive HTTP tokens per RFC 9110 §9.1; extension methods are accepted.
path: { exact: string }Case-sensitive equality. /v1/responses/ differs from /v1/responses.
Query strings/v1/responses?stream=true matches the path /v1/responses per RFC 9112 §3.2.1. Put no query or fragment in exact.
Non-matching requestsForwarded without this rule’s transforms, including credential injection. Matching does not deny origin access.

The edge compares the original method and raw URI path before reading the body. It does not percent-decode paths, collapse dot segments, or normalize slashes. Use the spelling your client sends. Thus GET /v1/models can pass through with its original body and headers while POST /v1/responses receives both the model patch and injected credentials. Unmatched requests retain client-supplied headers.

Freestyle currently supports method and exact path only. Unsupported fields are rejected. match is accepted on HTTP egress rules to a public origin or host; it does not change domain/source rule selection, choose another destination, or fall through to another TLS rule. Match conditions are visible on reads, so do not put secrets in them. Updates replace the full rule: include match to retain it, or omit it to restore unconditional transforms.

Transform JSON Request Bodies

For complete provider recipes, see OpenAI models and authentication, Anthropic models and authentication, OpenRouter models and authentication, and migrating from OpenShell.

A jsonPatch transform applies standard RFC 6902 JSON Patch operations to HTTP egress request bodies. Authentication stays in a separate headers transform, and destination selects the upstream:

await freestyle.tls.rules.create({
  action: "allow",
  domain: "inference.local",
  source: { vmId: "vm-123456" },
  destination: { host: "api.openai.com", port: 443 },
  match: { method: ["POST"], path: { exact: "/v1/responses" } },
  transform: [
    { headers: { authorization: `Bearer ${providerKey}` } },
    { jsonPatch: [{ op: "add", path: "/model", value: "approved-model" }] },
  ],
});

Run this in your trusted controller. The guest calls https://inference.local/v1/responses with its usual JSON request. A client SDK that requires an API key can use a placeholder; the edge injects the real key. For OpenAI Chat Completions, change the matched path to /v1/chat/completions. For Anthropic Messages, match /v1/messages, select api.anthropic.com, and inject x-api-key; the client still supplies required headers such as anthropic-version. The /model patch itself is the same.

add on an object is an upsert: it creates /model when absent and replaces it when present. It does not mean “set only if missing.” The six operations use standard semantics:

OperationBehavior
addInsert or overwrite an object member; insert an array element, shifting later elements. /- appends to an existing array.
replaceReplace an existing value, including an array element without shifting.
removeRemove an existing value.
copyCopy the value at from to path, with add destination semantics.
moveRemove the value at from, then add it at path.
testRequire an existing value to equal value; a failed test rejects the request.

Paths use RFC 6901 JSON Pointer. For example, /messages/0/content selects the first message’s content, ~1 escapes /, and ~0 escapes ~. The empty path selects the whole document. Parent objects and arrays must exist. There are no wildcards, automatic parent creation, conditional defaults, or foreach operations. null is an ordinary value; use remove to delete a field. Removing the document root or moving from the root is unsupported; use add or replace with an empty path to replace it.

{ jsonPatch: [
  { op: "add", path: "/model", value: "approved-model" },
  { op: "replace", path: "/messages/0/content", value: "Updated first message" },
  { op: "add", path: "/tools/-", value: extraTool },
] }

Operations run in order. Every operation must succeed before the request is forwarded. The example requires an existing first message and tools array. Use one jsonPatch per rule, alongside any header transforms, on HTTP egress from a VM or VPC to a public origin or host. Patches accept at most 128 operations and 64 KiB of serialized configuration. Operation value fields are write-only: sealed at rest and returned as "***". Operation names, path, and from remain visible; do not put secrets in pointer paths.

Without match, the patch applies to every request on the selected TLS rule. With match, only matching requests receive the patch and injected headers. JSON Patch does not provide a provider adapter or complete model-access policy. A /model patch does not constrain Anthropic fallback models, nested batch entries, or models selected by an uploaded JSONL file. Fixed nested fields can be addressed explicitly; arbitrary-length collections need a gateway with richer policy. It does not translate between OpenAI and Anthropic formats or validate provider-specific schemas.

Requests selected for JSON Patch must be UTF-8 application/json (an optional UTF-8 charset is accepted). Non-JSON and compressed bodies get 415; WebSocket upgrades and CONNECT get 403. Empty bodies, malformed JSON, request trailers, failed operations, or complexity/output-limit violations get 400. The edge bounds input and each intermediate/output document to 32 MiB, with at most 262,144 JSON values and 64 levels of nesting. Copies also reserve space for their source before allocation. Oversized incoming bodies get 413. The body must arrive within 30 seconds (408); a busy edge can return 503. The upstream must return response headers within five minutes (504). Responses, including SSE, stream through without JSON rewriting or body buffering. JSON whitespace and object ordering may change; numeric precision is preserved. Duplicate object names use their last value and are written upstream once. Scope the transform with match to let unrelated multipart uploads and body-less retrieval pass through without patching or credential injection. Matching multipart, JSONL, and WebSocket requests remain unsupported. Ordinary JSON HTTP inference, including requests asking for streamed responses, works with this transform.

Use freestyle.tls.rules.update(ruleId, fullRule) to change the patch for later requests. Supply the entire rule, including actual patch values and header secrets. A redacted read cannot reconstruct them. Cached rules can take about five seconds to refresh; updates do not alter requests already in flight. Editing JSON Patch or matched rules in the dashboard currently requires the SDK or CLI, preserving write-only values and request conditions.

The CLI accepts the same operation array through --json-patch on tls create and tls update, composable with --header and the JSON --match option:

freestyle tls create --domain inference.local --from vm=vm-123456 \
  --to host=api.openai.com,port=443 \
  --header 'authorization=Bearer sk-…' \
  --match '{"method":["POST"],"path":{"exact":"/v1/responses"}}' \
  --json-patch '[{"op":"add","path":"/model","value":"approved-model"}]'

Transforms keep the normal firewall semantics: omit broad public-egress grants when the guest must use only its named TLS routes. A transform affects its selected route, not other routes the VM can access.

Send A VM Out Through Your Own Proxy

A VM’s outbound sessions leave through a SOCKS5 proxy you supply. The edge authenticates to the provider; the VM never holds the credentials.

// Everything vm-123456 opens leaves through gate.provider.com.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "*",
  source: { vmId: "vm-123456" },
  destination: { host: "gate.provider.com", port: 7000 },
  transform: [{ socks5: { username: "cust-9", password: "…" } }],
});

protocol is inferred from the transform. The destination is the proxy’s endpoint, and both host and port are required — { public: true } is refused.

A catch-all rule sets all_proxy and ALL_PROXY in the guest, in /etc/environment and in /etc/profile.d/freestyle-socks5-proxy.sh. exec, PTY and SSH sessions all inherit them:

curl https://api.ipify.org   # answers with the provider's address

The endpoint is socks5h://socks5.freestyle.internal:1080. Point a client at it directly to use it without the environment. Use socks5h, not socks5: the proxy must receive the hostname, and the hostname selects the rule.

domain on a socks5 rule is an allow-list of destinations, checked against what the guest asked for in the SOCKS request. * allows any destination and is accepted here only. A narrower rule allows what it names and nothing else:

// vm-123456 reaches anything under target.com through the proxy.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "*.target.com",
  source: { vmId: "vm-123456" },
  destination: { host: "gate.provider.com", port: 7000 },
  transform: [{ socks5: { username: "cust-9", password: "…" } }],
});

Only a catch-all rule writes the guest’s proxy environment. A narrower rule leaves it alone; aim a client at the endpoint yourself.

The transform is optional. A provider that allow-lists by source address needs no username or password.

The edge terminates nothing here: it opens the connection through the proxy and copies bytes. Your TLS runs end to end to the origin, no platform CA is installed in the VM, and no certificate is issued.

socks5 is egress only. Its password is write-only: sealed at rest, read back as "***".

Connect One VM To Another By Name

A rule whose destination names another VM is an internal service — the same edge-brokered path as a public domain, pointed inward. The source opens https://the-name; the platform edge terminates it, presenting a certificate the guest trusts, and forwards to the target VM’s port. You address the service as plain HTTPS and the edge maps it to whatever port the backend listens on.

// vm-web reaches vm-db's admin UI as "db.internal": https://db.internal
// terminates at the edge and forwards to vm-db:8000.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "db.internal",
  source: { vmId: "vm-web" },
  destination: { vmId: "vm-db", port: 8000 },
});

Why the edge, and not a direct VM-to-VM hop? The certificate has to be resolved outside the guest. A VM could only present a platform-trusted certificate for the name by holding the platform’s private key — which no guest ever does — so the edge is the one place the name can terminate. Two grants make it reach: the firewall admits the source’s packets to the edge, and the standing platform grant already lets the edge reach the target VM. Both are grants, not gates — a VM that already had a path is unaffected.

An internal service can also be Postgres. Set protocol: "postgres" and the source connects to the name on 5432 instead of opening https://the-name:

// vm-web reaches vm-db as "db.internal" on 5432.
await freestyle.tls.rules.create({
  action: "allow",
  domain: "db.internal",
  protocol: "postgres",
  source: { vmId: "vm-web" },
  destination: { vmId: "vm-db", port: 5432 },
});

An internal service carries no transform, whatever its protocol.

Only exact names steer this way — the guest’s /etc/hosts has no wildcards. A *.suffix or catch-all internal name needs the platform resolver.

Source And Destination

TLS rules allow connections; they do not block other network access. To restrict a VM to its named routes, omit broad public-egress grants from its firewall. Adding a TLS rule does not remove an existing network path.

A source identifies the VM or VPC allowed to open the session. A destination names one place for it to land:

FieldUse
source.vmIdOne VM may connect.
source.vpcIdEvery member of the VPC may connect.
destination.publictrue selects the domain’s public origin.
destination.hostPin the upstream hostname; also supply port.
destination.vmIdConnect to another VM; also supply port.

A source cannot carry host or port; a destination cannot be a vpcId. Choose exactly one destination identity: vmId, host, or public: true. The edge authenticates the source by its switch port and anti-spoofed address, not by the name the client dials. A blank endpoint never means the Internet; public: true must be explicit.

Declaring Rules With A VM

In vms.create, use source: {} for the VM being created:

const { vm } = await freestyle.vms.create({
  firewall: { rules: [] },
  tls: {
    rules: [{
      action: "allow",
      domain: "api.openai.com",
      source: {},
      destination: { public: true },
      transform: [{ headers: { authorization: "Bearer sk-…" } }],
    }],
  },
});

The empty firewall grants no raw-IP Internet access; the TLS rule supplies the path to this domain through the edge. Exactly one end must omit its identity. If an inline rule is invalid, the whole create fails and no VM is made. Use tls.rules.create for a rule between existing resources.

Rotating A Secret

Rules are replaceable in place — the one way this API diverges from the firewall’s create/delete immutability. A transform carries a rotating secret, and delete-then-create would drop traffic or trip your account’s rule limit mid-swap. update keeps the rule’s id and creation time and replaces everything else:

// Rotate the injected key without taking the path down.
await freestyle.tls.rules.update("tls-123456", {
  action: "allow",
  domain: "api.openai.com",
  source: { vmId: "vm-123456" },
  destination: { public: true },
  transform: [{ headers: { authorization: "Bearer sk-rotated" } }],
});

Because secrets never read back, an update always states the full transform — there is no “keep the old value”. A read carries redacted: true when it has hidden a secret, so you know the shape you see is not the whole story.

List, Get, And Delete

// Every rule in your account, newest first.
const { rules } = await freestyle.tls.rules.list();

// The rules that apply to one VM: those naming it, plus those naming a
// private network it is on.
const { rules: applied } = await freestyle.tls.rules.list({ vmId: "vm-123456" });

// Or the rules naming one network.
await freestyle.tls.rules.list({ vpcId: "vpc-backend" });

const rule = await freestyle.tls.rules.get("tls-123456");

await freestyle.tls.rules.delete("tls-123456");

From The CLI

freestyle tls create --domain api.openai.com --from vm=vm-123456 --to public \
  --header 'authorization=Bearer sk-…'
freestyle tls create --domain db.vendor.com --from vm=vm-123456 --to public \
  --pg-user app --pg-password 'hunter2' --pg-database prod
freestyle tls list --vm vm-123456
freestyle tls update tls-123456 --domain api.openai.com --from vm=vm-123456 --to public \
  --header 'authorization=Bearer sk-rotated'
freestyle tls delete tls-123456

Each endpoint uses comma-separated key=value pairs (vm, vpc, host, port) or the bare word public. Repeat --header name=value for each injected header; values are write-only. --pg-user, --pg-password, and --pg-database build a Postgres transform, and --socks5-user and --socks5-password build a proxy transform. These protocol-specific transforms are mutually exclusive with --header.

Lifecycle

A rule cannot outlive what it names. Delete a VM or a private network and the rules referencing it go too — the same cascade the firewall uses — so you never end up with a rule pointing at a machine that no longer exists. Every rule reports what it depends on:

const rule = await freestyle.tls.rules.get("tls-123456");
console.log(rule.dependencies);
// [{ kind: "vm", id: "vm-123456" }]

The dependency runs one way. Deleting a rule never touches the VMs or networks it named.

Limits

  • Only exact names steer. The guest’s /etc/hosts cannot steer wildcard or catch-all egress, including wildcard internal services. SOCKS5 is the exception: its destinations are selected inside the SOCKS request, so wildcard and catch-all rules work.
  • SOCKS5 requires a proxy destination. Supply host and port; destination: { public: true } is refused.
  • tcp, imap, and imaps are public ingress only. They cannot be used for outbound or VM-to-VM rules.
  • Postgres channel binding upstream is not available. A brokered session has two TLS connections, so an origin offering only SCRAM-SHA-256-PLUS is refused. Plain SCRAM-SHA-256 is supported. CancelRequest is not routed.
  • Internal services cannot carry transforms. The API refuses them on VM-to-VM rules.
esc