A config-driven CLI for running a personal dev box on Google Compute Engine —
and for hosting your side projects on it. Your terminal is the control plane:
no hosted service, no daemon on the VM, nothing between you and your machine
but gcloud and SSH.
You describe the box in one versioned file — system packages, dev tools, dotfiles, long-running services, public domains — and bastion converges the VM to it. Idempotently, with a read-only plan first, over a private-by-default connection.
$ bastion up dev # start the VM, wait for SSH, converge, wait for health
$ bastion ssh dev # interactive shell over IAP — no public SSH port
$ bastion plan dev # what would apply change? (read-only)
$ bastion apply dev # packages, tools, dotfiles, services
$ bastion port dev metrics:web # private endpoint → localhost tunnel
$ bastion service logs dev blog -f # tail a service
$ bastion ssh-config dev --install # reach the box from any SSH tool (IDEs, scp, rsync)
$ bastion audit dev # hardening check-up: real risks only, exact fixes
$ bastion down dev # stop compute; disks and data stay$ brew install sanketsaurav/tap/bastionOr go install github.com/sanketsaurav/bastion/cmd/bastion@latest, or grab a
signed archive from the
releases page
(verification instructions in SECURITY.md).
- The gcloud CLI, authenticated against your project.
- A GCE VM running Ubuntu 24.04 LTS with IAP + OS Login access. bastion
attaches to a VM you own — it never creates, resizes, or deletes cloud
resources, so your VM can come from the console,
gcloud, or Terraform.
Not sure your setup qualifies? bastion doctor checks everything — gcloud,
IAM, connectivity, the guest OS, sudo, egress, Docker — and every failure
comes with the exact fix.
$ bastion init ~/boxes/dev # scaffold a definition
$ $EDITOR ~/boxes/dev/bastion.yaml # point it at your VM
$ bastion box adopt dev --config ~/boxes/dev
$ bastion doctor dev # verify everything before touching anything
$ bastion up dev # start → reachable → converged → healthy
$ bastion ssh devEverything bastion manages comes from bastion.yaml — there is no imperative
command that creates hidden state. A working example:
apiVersion: bastion/v1alpha1
kind: Box
metadata:
name: dev
provider:
name: gcp
project: my-project
zone: us-central1-a
instance: my-devbox
host:
packages: [git, jq, tmux]
features:
- uses: docker
- uses: github-cli
- uses: claude-code
- uses: mise
files:
- source: files/tmux.conf
target: ~/.tmux.conf
mode: replace
shell:
prompt: alice # PS1 shows alice@dev, not ext_alice_gmail_com@dev
motd: quiet # no Ubuntu login wall of text (or ads)
ingress:
baseDomain: apps.example.com # one wildcard DNS record serves every app
volumes:
blog-data: { persistence: durable }
secrets:
blog-admin-token:
source: { file: ~/.secrets/dev/blog-admin-token }
services:
blog:
image: ghcr.io/example/blog:1.4.2
environment:
ADMIN_TOKEN: { secretRef: blog-admin-token }
mounts:
- { volume: blog-data, target: /data }
endpoints:
web:
containerPort: 8000
visibility: public # → https://blog.apps.example.com, cert included
auth: none # explicit: the app owns authentication
metrics:
image: ghcr.io/example/metrics:2.1.0
endpoints:
web:
containerPort: 3000
visibility: private # loopback-only; reached via `bastion port`bastion plan shows exactly what apply would do before it does it. Applies
are idempotent (a second run is a no-op), resumable after interruption, and
additive: removing a package or tool from the definition stops managing
it but never uninstalls it — plans report such orphans, and
bastion feature remove cleans up user-level tools when you ask (your
configuration and credentials are always kept).
Built-in features: docker, github-cli, tmux, build-essential,
mise, uv, bun, claude-code, codex — plus your own local feature
scripts. bastion config schema prints the full JSON Schema.
Any HTTP service can get a real domain with automatic HTTPS:
- Declare
ingress.baseDomainand mark an endpointvisibility: publicwith an explicitauthpolicy — the policy is your acknowledgement that the endpoint faces the internet. - One-time setup, guided and verified by
bastion doctor: a static IP for the VM, a wildcard DNS record (*.apps.example.com → <IP>), and an open 80/443 firewall path. bastion never touches DNS, IPs, or firewall rules itself. bastion apply. The service is live athttps://<service>.<baseDomain>with a real certificate — bastion runs a managed Caddy proxy that routes by hostname and issues per-host certificates automatically.
After the one-time setup, hosting the next app is purely a yaml change. Use
hostname: on an endpoint for custom domains, and bastion endpoint list
to see every URL. Your services never bind a public port themselves — only
the hardened proxy faces the internet.
Secrets come from local files or environment variables on your machine, resolve only at apply time, and land in root-owned env files on the box. They never appear in the definition, plans, logs, generated configs, or digests — which also means changing a value is invisible to a normal plan:
$ bastion apply dev --rotate-secrets # re-resolve values, replace the containers using themDurable volumes live on the box's data root and survive down/up and
container replacement. Deleting data is never a side effect: removing a
service orphans its volume (reported in plans), and bastion volume delete
requires a confirmation naming the volume.
bastion talks to GCP exclusively through your installed gcloud (your auth,
your IAP tunnels) and to the guest over SSH — multiplexed, so the first
connection pays the tunnel setup and every command in the next ten minutes
rides it near-instantly. For each plan or apply it
generates a bash program, pipes it to the VM, and reads back a structured
event stream — nothing is installed or left running on the box, and every
mutation is recorded in per-resource marker files so partial failures resume
cleanly. Remote commands are strictly quoted; nothing user-supplied is ever
interpreted by a shell unless you pass --shell.
What bastion will never do: create or destroy cloud resources, modify
firewall rules or IAM, uninstall things because you undeclared them, bind
your services to 0.0.0.0, or put a secret in a log. The full contract is
in SPEC.md.
Every command accepts --json for machine-readable output.
Early software (v0.x): the configuration API is bastion/v1alpha1 and may
change between minor versions until 1.0. Current scope: attached mode (your
VM), Ubuntu 24.04 guests, Docker runtime, GCP. The
specification is decision-complete and documents what's deferred.
See CONTRIBUTING.md. Development needs no GCP access — the whole suite runs against fakes.