- has no inbound SSH port. Laptops reach sshd through AWS Session Manager, so the security group admits nothing from outside the org’s own boxes, which reach each other on TCP 14650.
- never runs its user-data. The user-data is three fields of JSON: the org id, box id, and control plane URL. None of it is secret. The image turns cloud-init’s user-data processing off, so nothing in it is executed. On first boot a step baked into the image reads it from the instance metadata service, refuses to go on unless every field is well formed (an https control plane URL), fetches boxd, and installs it only if its release signature verifies.
- holds no Tesser credential. boxd proves which instance it runs on
with a short-lived token that AWS STS signs for the box’s role
(
sts:GetWebIdentityToken, outbound identity federation). The control plane accepts it only when the token names this box’s instance in your account, addressed to the control plane. - launches only the image your role allows. By default that is Tesser’s published image: your role may launch images owned by Tesser’s publishing account and no one else’s. To own the image outright, copy a release into your account and pin the role to the copy (Own the image).
Set it up
You need to be an admin of the org, and the org needs to have no boxes: a box cannot move between clouds, sotesser rm any it has first.
The setup is a Pulumi
program you copy into your own infrastructure repository; it needs a
@pulumi/aws 7 release with aws.iam.OutboundWebIdentityFederation. It is two files:
tesser-policy.ts holds the IAM documents, and index.ts makes the
network, the roles, and the instance profile.
Two values come from Tesser:
tesser-policy.ts
index.ts
pulumi up finishes, run the command it exports as connect, with
the ~/.aws profile members’ laptops use for
Session Manager:
--account finds the role (tesser-<org id>), the boxes’ instance profile
(tesser-<org id>-box), and the subnet and security group tagged with the
org id. The account id can be pasted with dashes, the way the AWS console
shows it. From then on every
tesser make, dev, and exec launches in your account. Boxes sleep,
wake, and are removed exactly as before.
The program makes, in the region you run it in:
- A VPC (
10.73.0.0/16) with one public subnet, an internet gateway, and a route to it. The subnet sits in a zone that offers every box size:use1-az1in us-east-1,usw2-az1in us-west-2. Boxes need that egress to reach Session Manager andapi.tesser.sh. - A security group: TCP 14650 from the group itself so boxes reach each other, all outbound, and nothing else inbound. No port 22.
- The identity provider for
https://api.tesser.sh. An account holds one per URL; when a second org sets up in the same account, drop theOpenIdConnectProviderfrom its program and look up the existing one withaws.iam.OpenIdConnectProvider.get. - A role named
tesser-<org id>that only this org can assume, with the permissions policy fromtesser-policy.ts. - Outbound identity federation, switched on for the account, so the boxes’ role can ask STS for identity tokens. It is one account-wide setting; a second org in the same account drops it from its program.
- The boxes’ role and instance profile,
tesser-<org id>-box, withAmazonSSMManagedInstanceCore,sts:GetWebIdentityTokenfor tokens addressed tohttps://api.tesser.shonly and, withtesser:parameterStore, read access to the org’s parameters.
pulumi destroy removes everything it made;
disconnect the org first.
To use an existing VPC, a private subnet behind a NAT (the CLI then reaches
boxes at their private IP with --private), or your own role names, keep
tesser-policy.ts and change index.ts, then connect with the ids
(--role, --subnet, --sg, --instance-profile) instead of
--account. tesser cloud template --account <id> --region <region>
prints the same policies with your values filled in. PassBoxRole names
the IAM role inside the boxes’ instance profile, not the profile. The
program gives the role and the profile the same name, and template
assumes that: it uses the org’s instance profile name (or
tesser-<org id>-box before the org is connected). If your role’s name
differs from the profile’s, or has a path, pass it as boxRole (in the
program) or --box-role path/name (to template).
Own the image
By default boxes launch Tesser’s published image, and the role may launch any image Tesser’s publishing account owns. To take that choice away from Tesser, copy an image release into your account and pin the role to the copy. Tesser then launches exactly that image and no other, and a new release reaches your boxes only when you copy it and move the pin. Every image build of github.com/tesser-sh/ami is a release namedbuild-<run id>.
Its tag points at the commit that records the build’s AMI ids in
amis.json, one per region: { "us-east-1": "ami-...", "us-west-2": "ami-..." }.
Pin that commit, never main: the sha is what pins the release, and it is
the tree to review. This prints the latest release’s commit:
image line of index.ts with:
amisSha to a newer release’s commit to take a new image.
The permissions policy’s LaunchImage statement becomes
arn:aws:ec2:<region>::image/<your copy> with no owner condition, and
tesser:imageOwner is no longer needed. Copy a release soon after you pin
its sha: Tesser keeps only its two newest images public, and an older
source can no longer be copied. Then tell the control plane which image to
launch:
--image tesser goes back to the published image; move the policy back
first. The image can change while boxes run: running and sleeping boxes
keep the image they launched from, and new boxes take the new one.
Serve shared boxes on your own domain
A share gateway makes every shared box answer athttps://box-<id>.<host>/
for anyone on your network, with no Tesser login: a teammate, a designer, or
a phone on the VPN opens the URL. The first port the box’s service declares
is served there, and every other port at https://box-<id>--<port>.<host>/,
so each port is its own site the way localhost:<port> is. It is one
process, tesser gateway serve, on a small instance in the boxes’ VPC,
behind an internal Application Load Balancer that terminates TLS with an ACM
certificate for *.<host>. Opening a sleeping box’s URL wakes it, with a
page that shows the progress and turns into the app when it answers. The
gateway proxies HTTP and WebSockets to the box’s mesh port and keeps the box
awake while traffic flows. A box is served only while it has a live share:
tesser share <box> prints the box’s gateway URL next to the review link, and
tesser share <box> --revoke, or the share expiring, takes it off within 15
seconds.
The gateway does no user auth. Whoever can reach it reaches every shared
box, so admit only your VPN or an SSO proxy to it.
It proves itself to Tesser with its IAM role’s AWS web identity token
(sts:GetWebIdentityToken, the same outbound identity federation the boxes
use) and holds no Tesser secret.
Add this to index.ts, with the host the boxes hang off and the CIDR your
people reach the VPC from:
<host>’s public DNS carries a validation
record: pulumi up exports it as certificateValidation. Add it to your DNS
once and leave it there, and ACM renews the certificate on its own. A <host>
with no public DNS can’t get an ACM certificate; import one into ACM instead
and pass its ARN with pulumi config set tesser:gatewayCertificate <arn>.
gateway.ts
pulumi up, run the command it exports as gatewaySet. The load
balancer’s group admits HTTPS from gatewayFrom and sends only to the
gateway’s port; the gateway’s group admits only the load balancer. The
gateway’s egress is HTTPS (Tesser, STS, Session Manager) and the boxes’ mesh
port, which the boxes’ group admits from it. The instance carries no tesser:org tag, so Tesser’s
role can neither stop nor sweep it. The hosted zone is private to the VPC:
forward <host> to it from your VPN’s DNS (a Route 53 Resolver inbound
endpoint, or Tailscale split DNS), or point a wildcard record in your own
DNS at the load balancer.
The dev server sees what it sees on a laptop: the gateway presents each
request as http://localhost:<port> (with a prefix in front of the box’s
label kept, as <prefix>.localhost), so Vite’s allowedHosts and Next’s
allowedDevOrigins need nothing added. A redirect the app builds to
http://localhost:<port> goes back to the browser as the gateway URL, and
the gateway adds a small script to each page that sends the page’s own
fetch, XMLHttpRequest, WebSocket, and EventSource calls to
localhost:<port> through the gateway, so a frontend that calls its API at
localhost:5000 works unchanged. Links, forms, and image and script
sources that point at localhost:<port> are rewritten too, whether they
come in the page’s HTML or are added by its scripts (a script-added link or
form is fixed when it is clicked or submitted). Each port’s
site keeps its own cookies, where localhost shares them across ports, and a
third-party sign-in only returns to the gateway URL if the provider allows
it as a callback. Only HTTP and WebSockets cross the gateway.
What connect checks
connect tests the account end to end and stores nothing if any check
fails. Each failure names what is wrong and how to fix it:
tesser cloud check runs them again at
any time without changing anything. tesser cloud shows the config, and AWS
errors from the last seven days: a call AWS keeps refusing, such as a
StopInstances a tightened policy no longer allows, stays listed until it
works again. A dry run cannot test stopping or terminating an instance that
does not exist yet, so those only surface there.
What the control plane can do
Your code, the disks, the dev server logs, and the traffic between boxes stay in your account. Where your code goes lists every hop and everything the control plane receives. What the control plane can still do to your boxes, through the role and boxd’s WebSocket:- Start, stop, and terminate the org’s instances, and launch new ones from the image the role allows, into your subnet and group.
- Start and stop the dev server declared in your manifest. A start names only the service; boxd reads the command from the manifest on the box’s own disk.
- Set its env. boxd drops keys that load a program (
NODE_OPTIONS,LD_PRELOAD,BASH_ENV,PROMPT_COMMAND, theGIT_andNPM_CONFIG_families, and more). The list is a denylist, so it covers the known hooks, not every program’s. With Parameter Store the control plane can write values but not read them. - Update boxd. An update makes boxd fetch a new bundle from the control plane, and boxd installs it only if it carries Tesser’s release signature; first boot checks the same signature. The control plane can pick which signed release runs, not write its own.
- Replace
authorized_keys. sshd has no network path except Session Manager, which your IAM gates, so a key alone reaches nothing.
How Tesser reaches your account
Tesser never holds a credential for your account. The control plane is an OpenID Connect issuer, and the role trusts it for exactly one org id. Each time the org cell needs EC2 it signs a token that lives five minutes, trades it for one hour of temporary credentials withAssumeRoleWithWebIdentity,
and uses those. Another org’s cell cannot assume your role: the trust policy
names the org in sub, and the token carries the org that signed it. The
permissions are fenced the same way: the role can only launch instances
tagged with the org id, and only stop, start, or terminate instances that
carry it.
Session Manager
Every ssh, sync, and daemon tunnel runstesser ssm-proxy as its
ProxyCommand: tesser’s own Session Manager client, so no AWS CLI and no
session-manager-plugin on the laptop. The host key is filed under the
instance id.
What it needs:
- The instance profile carries
AmazonSSMManagedInstanceCore(the SSM agent ships in the Tesser image), and the role tesser assumes hasiam:PassRoleon its role. The program above does both. - Every org member needs an AWS profile in
~/.aws/config(or~/.aws/credentials) for the account, withssm:StartSessionandssm:TerminateSessionon the boxes. Any profile kind works (SSO, static keys,source_profile+role_arn,credential_process); tesser reads the same files and SSO token cache the AWS CLI does, soaws sso login --profile <name>is still how an SSO profile signs in.--aws-profile <name>oncloud connectis the org’s default profile name, the one every member’s laptop is expected to have; a laptop whose profile is named differently overrides it withtesser cloud aws-profile <name>, kept in that laptop’s tesser config. With--sso-start-url <portal> --sso-region <region> --sso-role <permission set>oncloud connect, members don’t have to set anything up: a laptop missing the profile, with nothing else for the account, gets it appended to~/.aws/config(account from the role ARN) the first time it makes or wakes a box, and the SSO sign-in below takes it from there. A profile the laptop already has is never touched. - Agents and CI don’t need the profile. When it is missing or will not
resolve, tesser uses the default AWS credential chain (env keys, an
instance or container role, the default profile) once STS says those
credentials are for the org’s account; credentials for any other account
are never used. When neither works (SSO session expired, keys rejected),
every ssh-driven verb fails immediately and the
daemon holds its tunnels until it works again. For an SSO profile the verb
also opens
/__tesser/aws?profile=<name>on the daemon’s port: the daemon starts the device-code sign-inaws sso login --use-device-codedoes, shows the code, links to the AWS approval page, and writes the token to~/.aws/sso/cachewhen it is approved, so no AWS CLI is needed. The tunnels resume on their own. The verb’s error tells an agent to runtesser aws-wait <name> [--timeout 90s], which returns once the code is approved and fails with the reason if it is denied or expires.
ManageOwnSessions is the statement to get right. Scoping it by session ARN
the way AWS leads with
only matches an IAM user: a federated caller’s ${aws:userid} is
AROAEXAMPLEID:you@example.com while the session id is
you@example.com-<suffix>, so every terminate is denied, and silently, since
tesser ignores a failed close, leaving sessions Active until SSM times them
out. The session-tag form
above works either way, with the same scoping: your own sessions only.
Laptop-to-box transfers (sync, git packs) run at roughly link speed through
Session Manager: tesser’s client sends 32 KiB frames with a 4 MiB window,
where the official plugin paces itself to ~0.7 MB/s. Box-to-laptop is paced
by the SSM agent on the instance at about 0.7 MB/s, which logs and
exec output never approach.
Env in your Parameter Store
By defaulttesser env set stores a service’s values in the org, and the
control plane injects them when an instance starts. With
--env parameter-store they live in your account instead, one SecureString
per key under /tesser/<org id>/<service>/<KEY> in the org’s region,
encrypted with the account’s aws/ssm key:
tesser env set/unsetgo through the control plane, which writes the value into your Parameter Store and keeps only the key’s name, sotesser env lsand theenv.requiredcheck need no AWS call. The role it assumes needsssm:PutParameterandssm:DeleteParametersonparameter/tesser/<org id>/*and nothing else: nossm:GetParameter*, nossm:DescribeParameters. That one statement is what you audit. A parameter changed or removed outsidetesser envis not noticed byenv lsor theenv.requiredcheck; boxes see it the next time they read.- Each box reads the values itself with the instance profile’s
credentials: when its dev server starts (a service box reads its
service’s, a workbench every service of its repository, as with
cell-held env) and, for shells, on connect and after every
env set. That profile’s role needsssm:GetParametersByPathon the same prefix; the Pulumi program adds it withtesser:parameterStore. Any box in the org can read any of the org’s services’ env, which is the same trust boundary as the mesh.
connect proves the role can write and delete under the prefix before
storing anything, then moves the values the org already holds into
your Parameter Store and forgets them. A value is at most 4 KB (the standard
tier), and may not be blank; unset a key to remove it. If a box cannot read
its env at start, the run fails with the AWS error in tesser dev’s output
rather than starting without it. Moving --env back to tesser, or
disconnect, leaves the parameters in your account and the org with no
values: set them again. If you later move back to parameter-store, boxes
read whatever is still under the prefix, including values the org no longer
lists; delete the stale ones or set them again. tesser service rm deletes the service’s
parameters. Because these are your parameters, the control plane can also
set them, the same way it can start the dev server; what it cannot do is
read one.
Changing or leaving
tesser cloud connect --max-boxes <n>, --env <store>, --image <ami>,
--aws-profile <name>, and --no-crash-output (boxd keeps a crashed dev
server’s last lines on the box instead of sending them with its exit) work
at any time; the other flags default to the
stored config. Where boxes run cannot change while any exist. Remove them,
then connect again with the new ids, or tesser cloud disconnect to
return the org to Tesser’s cloud.
Disconnecting deletes nothing in your account. pulumi destroy removes the
network, the roles, and any image copy when you no longer need them.