Everything it takes to run Attestree.
One container and a database on a server you own. Windows endpoints that only ever dial out, and still pick up a package change within about a minute. An optional detonation host. And a short list of public hosts, each off by default, used only when you act, or switchable. This is the whole map: every box, every connection, every port.
Who talks to whom.
- inside your network
- mutual TLS
- outbound to the internet
- optional / your call
Scroll the map sideways →
Shown: the self-hosted Community Edition. The commercial tier keeps the same endpoints, agent and detonation side, but its server side is a different build in your own Azure subscription, with its own map on the dedicated instance page.
Nine wires, and what rides on each.
The numbers match the map. Arrows point from whoever opens the connection, which is what your firewall cares about.
-
1
Endpoint agent → Platform
HTTPS · your public URL — :18080 by default, :18444 with the Caddy overlay, :443 behind your own proxy
Enrollment; a wake probe about once a minute; a full check-in whenever the probe says something changed, and every 15 minutes regardless; signed-span and inventory upload; the ring-staged agent update; and the winget feed the agent registers as a machine-wide source. A change to an endpoint’s packages, ring or groups raises a wake for it, so it reaches the machine in about a minute without a port open on it; anything else, such as an agent-rollout step, lands at the next 15-minute check-in.
AUTH An enrollment token (single-use by default), then a session token that rotates on every check-in; the winget feed uses its own per-endpoint feed token; once the endpoint has a registered device key, each check-in is also signed with it. Desired state carries an HMAC keyed by that check-in’s session secret; the agent checks it and fails closed.
-
2
Operator browser → Platform
HTTPS · same port as the agents
The console: catalog, rings, fleet, audit, settings.
AUTH Local accounts, four policy-enforced roles, TOTP two-factor. Entra ID SSO on commercial tiers.
-
3
Platform → Postgres 18
Postgres wire · :5432 on the internal compose network
All operational state: catalog, fleet, rings and the signed audit log.
AUTH Password from your .env. The host binding is 127.0.0.1 only; do not publish it.
-
4
Platform → Public internet
HTTPS · :443 outbound
Downloads only: winget manifests and installers at ingest, the CVE and KEV feeds. The full allowlist is below.
AUTH HTTPS only. The CVE feed is limited to a pinned host set; every fetch is screened again on each redirect hop, private address ranges are refused, and an installer’s SHA-256 is checked as it streams in.
-
5
Detonation broker → Platform
gRPC over mutual TLS · host :18443 → container :8443
The broker takes detonation jobs over one long-lived stream, pulls each installer from the platform (never from the internet), and pushes back the result and derived artifacts. Only with the broker overlay switched on.
AUTH An enrollment token once, then a client certificate issued by the platform’s own broker CA. The broker pins the platform’s key instead of trusting the OS store, and listens on nothing itself.
-
6
Broker → Hypervisor
Local Hyper-V, or the Proxmox API · Proxmox: :8006 API + :8765 result extractor (mTLS)
Clones the golden image, starts the sandbox, harvests the result disk.
AUTH Proxmox: least-privilege API token and pinned TLS keys. The hypervisor is not trusted for integrity; the signed template record is.
-
7
Hypervisor → Sandbox VM
VM lifecycle · no connection to your server
One short-lived VM per package, cloned from your signed golden image. It installs the package, writes its result to its own disk and halts; the broker reads that disk.
AUTH The golden image is built from your own Windows ISO and signed with your own key. None is published.
-
8
Sandbox VM → Wherever the installer dials
whatever a real installer uses · offline by default
A sandbox starts with no network. On Proxmox it stays that way: the guest has no network adapter. On Hyper-V, an installer that needs the network gets its adapter connected, and its traffic is captured as evidence if you provisioned a recording proxy; without one it runs connected but unrecorded. Where that switch leads is your decision.
AUTH n/a. This is the thing under test.
-
9
Endpoint winget → Vendor download host
HTTPS · :443 outbound, from the endpoint
Only for a version whose installer bytes your server does not hold: one attested before the byte store was set up, or one whose kept bytes were since cleaned up. Everything detonated with the byte store on comes from your server instead.
AUTH winget refuses any file whose SHA-256 differs from the one pinned at ingest, so the vendor host can serve the bytes but cannot change them.
What it can touch, and what it cannot.
It drives VMs, so it looks powerful on a map. Its reach is deliberately small: one outbound connection to your platform, and its own hypervisor.
- Your platform, outbound only
- One gRPC connection to :18443: enroll once, keep a control stream open, pull installer bytes, push results back. Plus a best-effort “please re-enroll me” call to the same URL.
- Its own hypervisor
- Hyper-V: local cmdlets, as a dedicated local account in Hyper-V Administrators, to clone, start, stop and delete sandbox VMs and read their result disks. Proxmox: the PVE API on :8006 with a least-privilege token, and the result extractor on :8765.
- Its own files
- Its keys and state under %ProgramData%\Wep\Broker, and the golden image you point it at.
- The internet. Installers come from your platform, already pinned to a SHA-256. One opt-in exception: on Hyper-V, the recording proxy relays a sandbox’s allowlisted traffic.
- Your endpoints, your Postgres, your directory. It has no credentials for any of them.
- Anything from your network. It listens on nothing, with two exceptions: the Proxmox bring-up wizard opens a single-use HTTPS listener on one NIC, firewalled to the PVE host, and closes it when done; and the opt-in Hyper-V recording proxy listens on the sandbox-only switch.
- SYSTEM or domain rights. It runs as its own local account, not LocalSystem; on Proxmox the bring-up wizard uses the root password, read from an environment variable rather than a command-line flag, to mint that limited token.
What to open, and what to allow out.
Inbound
Only your server takes connections, and on Proxmox the PVE host, from the broker. Ports are the reference compose defaults; change any of
them in .env.
| ON | PORT | FROM | NOTES |
|---|---|---|---|
| Your server | tcp/18080 (or 443 / 18444 behind TLS) | Endpoints and operator browsers | The only port most deployments open. Endpoints that are not the Docker host itself need HTTPS: winget refuses a plain-HTTP source on anything but loopback. |
| Your server | tcp/18443 | Detonation broker(s) | Only with the broker overlay. TLS, mutual after a one-time token enrollment. |
| Your server | tcp/55432 | Nobody else | Postgres, bound to 127.0.0.1 by default for local backups. Keep it that way. |
| Proxmox host | tcp/8006, tcp/8765 | The broker VM only | Proxmox API and the result extractor. Only on Proxmox detonation. |
| Windows endpoints | none | n/a | The agent only dials out, and still picks up a package or ring change within about a minute through its wake probe. A detonation broker takes nothing from your network either. |
Outbound allowlist
Every host the platform can reach on its own, HTTPS on tcp/443 apart from its own URL. Each one is a download of public data or a destination you configured. Nothing of yours is uploaded unless you switch on the transparency log.
| FLOW | HOSTS | DEFAULT | IF BLOCKED |
|---|---|---|---|
|
Platform image pull
Install and upgrade, from the Docker host
|
|
You run it | Pull fails loudly. Mirroring the unmodified image to an internal registry is allowed. |
|
CVE corpus + CISA KEV
Scheduled sync
|
|
On | CVE data goes stale and says so after about a day. Nothing else breaks; an offline bundle import covers air-gapped sites. |
|
winget manifests
When you ingest a package, or armed unattended updates ingest one
|
|
On demand | Ingest is refused. Fail-closed. |
|
Version list
The ingest dialog
|
|
On demand | Type the version by hand instead. |
|
Installer download
When a broker detonates a version
|
|
On demand | That version cannot be detonated, and your server never holds its bytes. Fail-closed. HTTPS only. |
|
Package search
When you search in the ingest dialog; cached 6 h
|
|
On demand | Search shows an error. Paste a winget-pkgs URL or publisher.id@version instead. |
|
winget name index
About hourly once enabled, as a conditional GET
|
|
Off | Name and moniker lookup stays basic. A background refresh, separate from search above. |
|
Upstream monitors
Freshness, drift and withdrawal checks
|
|
Off | No freshness badges, drift detection or auto-heal. Gated twice: at deploy time and in the console. |
|
Rekor transparency log
Per attestation, once enabled
|
|
Off | n/a. The only upload path in the product, and it is off by default. |
|
Startup self-check
Once at boot
|
|
Always | A warning in the log. Never blocks anything. |
|
Installer byte store
Only if you choose Azure Blob in Settings › Storage
|
|
Off | n/a. Your own storage account, reached only when you configure it. |
These are the server’s. For every version that was detonated with the byte store on, an endpoint needs exactly one host: your server, which serves the attested installer bytes itself. A version attested before the byte store existed, or whose kept bytes were since cleaned up, is still downloaded by winget from the vendor host in its manifest (flow 9), pinned to the same SHA-256, so endpoints that install it need that host too. Why it matters is on the vendor CDN and proxy pages.
Three things, and one of them is optional.
One Docker host
- x86-64 (linux/amd64) Linux, or Windows / macOS with Docker Desktop. No arm64 image.
- Docker 24+ with Compose v2.
- One Compose stack: the platform and a Postgres 18 sidecar, a backup scheduler, and short-lived setup jobs that create the database role the platform connects as and take a backup before each upgrade. Backups need a recipient certificate you generate once.
- A URL your endpoints can reach, set as WEP_PUBLIC_BASE_URI before first boot.
Windows 11 machines
- Windows 11 23H2 or later, physical or VM.
- An agent MSI installed elevated with a one-time enrollment token. It is self-contained: no .NET runtime to install.
- Network reach to your server on one port, and trust in its TLS certificate (push the root by Group Policy or Intune; the agent never adds one). Nothing inbound.
- Trust in the Certum root that signs the agent and shim; pre-stage it on disconnected images.
A certificate for remote endpoints
- Same-host homelab: plain HTTP on localhost is fine.
- Anything else: HTTPS, because winget will not use a plain-HTTP source off loopback.
- Use the bundled Caddy overlay with your own certificate, or your existing reverse proxy.
- The platform adds no protocol or cipher knob. Endpoint TLS ends at your proxy, on its settings: Caddy’s defaults with the overlay.
A detonation host
- Hyper-V on Windows Server 2025, or Proxmox VE with a Windows broker VM.
- A golden image (about 40 GB) you build from your own Windows ISO and sign with your own key. On Proxmox the build host must be a separate machine.
- On Hyper-V, the broker installer also expects Docker Desktop on that host.
- Without one, nothing is verified, so by default no winget version is served.
- Sandboxes start offline; on Hyper-V, where a connected one leads is yours to choose.
Every switch that changes the map.
Defaults are the reference compose, as of platform v0.248.0. Config keys are set as environment variables with
__ for
: and need a restart; many are
listed read-only in the console under Settings › Deployment config. Console settings
are stored in the database and survive upgrades.
Where it listens
- Platform port
- 18080
- .env · WEP_PLATFORM_PORT
- Public URL agents call back to
- required
- .env · WEP_PUBLIC_BASE_URI
- Plain HTTP, no TLS
- off
- .env · WEP_ALLOW_PLAIN_HTTP the sample .env turns it on, for a localhost lab
- TLS front door
- none
- Caddy overlay, or your own proxy
- Trusted proxy networks
- built-in
- Console · Settings › Network 10.0.0.0/16 and 10.244.0.0/16 until you set your own; applies on restart
- Postgres host binding
- localhost
- WEP_POSTGRES_BIND / WEP_POSTGRES_PORT 127.0.0.1:55432, for psql and pg_dump on the host
Where things are kept
- Installer byte store
- local disk
- Console · Settings › Storage or Azure Blob, refused until you attest public access is blocked, versioning and delete protection are on
- Audit retention
- unbounded
- Console · Settings › Audit partition health whole months are pruned, and each prune is signed; pruning also needs Wep:Audit:Prune:Enabled, which ships off
- Installer bytes cleanup
- off
- Wep:Catalog:RetainedByteGc:Enabled while off, kept bytes are never deleted and storage grows
- Audit signing key
- local file
- Data volume a Key Vault in your subscription on commercial tiers
- Backups
- daily, 14 kept
- .env · WEP_BACKUP_SCHEDULE, _AT, _KEEP at 02:00 UTC: databases and keys, encrypted to a certificate you make once with backup keygen and keep off the host; one more before every upgrade. Installer bytes sit beside them unencrypted. Restore is not yet proven end to end: keep your own volume backups until it is
How packages get checked
- Backend
- manual SBOM
- Broker overlay + mint a broker in the console Hyper-V, or Proxmox/KVM through the wizard on the broker host
- Golden image in use
- none
- Console · Brokers, or Settings › All settings (advanced) the broker overlay seeds win11-pro-baseline; it must match what the broker advertises
- Parallel detonations per broker
- 1
- Broker config
- Recording proxy for sandbox traffic
- off
- Opt-in on the Hyper-V broker host you build its switch and image yourself; Proxmox sandboxes are always offline
What the server may reach
- CVE + KEV sync
- on
- Wep:Cve:Sync:Enabled changes hourly, a full refresh weekly; flagged stale after 24 hours
- Offline CVE bundle
- off
- Wep:Cve:BundlePath pin its signer with Wep:Cve:BundleTrustedPublicKeyPath
- Offline OSV bundle
- off
- Wep:Cve:OsvBundlePath pin its signer with Wep:Cve:OsvBundleTrustedPublicKeyPath; without one, an unsigned bundle is accepted with a warning
- winget name index refresh
- off
- Wep:Catalog:WingetIndex:Enabled name search at ingest still asks the winget CDN when you search
- Upstream freshness and drift
- off
- EgressAllowed in config, then Console · Settings › All settings (advanced) two gates: deployment and console
- Rekor transparency log
- off
- Wep:Rekor:Mode · Wep:Rekor:Endpoint point it at your own mirror instead of the public log
What runs without a click
- Unattended winget updates
- off
- Armed by an in-console ceremony
- Verifying the installer a waiting endpoint needs
- on
- Console · Settings › Rollout & safety since platform 0.242.1: hourly, at most 3 sandbox runs at a time, each downloading a vendor installer; a config off always wins
- Start an agent rollout on a new release
- off
- Console · Settings › Rollout & safety auto-arm; or arm a release by hand on the Agent rollout page
- Promote an agent rollout ring by ring
- off
- Wep:AgentRollout:Evaluator:FiringEnabled promotes healthy rings and halts a bad release; while off, an armed rollout does not move. Pause, resume and abort are in the console
- Rollback proposals
- off
- Console · Settings › Rollout & safety
- winget self-heal on endpoints
- off
- Console · Settings › winget self-heal never reboots on its own
- What an update does while the app is open
- Wait
- Per package: Wait, Remind, Ask then close, Close without asking Wait installs once the app is closed. Remind shows one notice per update. Ask then close gives the signed-in user a countdown they can postpone: by default a deadline 4 hours out, up to 3 postpones of 4 hours, then 15 minutes. Close without asking closes with no prompt, on unattended endpoints. An agent older than 0.248.0 reads Remind as Wait.
- Finishing an agent update that waits for a reboot
- on
- Console · Settings › Agent update resume without the reboot, after 72 hours, if the new agent already runs cleanly
- Emergency stop
- available
- Console, typed confirmation
Timing and limits
- Ring soak minimums
- 1 h to 7 d
- Console · Settings › Catalog policy canary 1 h, pilot 24 h, broad 72 h, all 7 days; per tenant, up to 30 days
- Unattended update dwell
- 24 to 72 h
- Policy › Update grant canary 24 h, pilot 48 h, broad 72 h; never shorter than the ring’s soak minimum; widening a grant takes a typed confirmation
- Highest ring automation reaches
- set per grant
- Policy › Update grant canary up to all (all needs an acknowledgement); packages and publishers can be excluded
- Pipeline sweep
- every 15 min
- Wep:UpdatePipeline a ring halts when more than 5% of its installs fail, over 14 days of evidence
- Updates in flight at once
- 3
- Policy › Update grant
- Waiver ceiling and expiry
- 90 days
- Console · Settings › All settings (advanced), platform admin only expiry 90 days, 180 at most; the file-count ceiling is twice the observed count, at most 1,000,000
- Export limits
- capped
- Console · Settings › Audit partition health 10,000 audit spans, 5,000 catalog versions, 100,000 inventory rows per export
Who gets in
- Local accounts, four roles
- on
- Console · Settings › Users & roles
- TOTP two-factor
- per user
- Console, with recovery codes
- Console session caps
- off
- Console · Settings › Users & roles commercial tiers enforce them from the start: admin 8 h or 1 h idle, other roles 12 h or 8 h idle. Off, a session is 8 hours, sliding
- Remember me
- 14 days
- Wep:Community:RememberMe sliding, never past 30 days
- Sign-in throttling
- on
- Wep:Community:AuthRateLimit 20 failed attempts per network per 5 minutes; an account locks for 15 minutes after 5 failures
- First-run setup token
- generated
- .env · WEP_COMMUNITY_SETUP_TOKEN pin your own for unattended installs
- Entra ID SSO
- commercial
- Set up with your instance optional Microsoft Graph role re-check
Not configurable, on purpose
OS and .NET defaults: SChannel on Windows, OpenSSL in the server container. A proxy you put in front is yours to tune.
No plugin seam, no algorithm setting. Standard primitives only.
Nothing to switch off, because nothing is there.
None, and no setting adds one. Package and ring changes still land in about a minute, because the agent asks.
Reaching a box is not the same as being believed by it.
Standard cryptography only, through the OS and .NET: TLS, ECDSA P-256 with SHA-256, HMAC-SHA-256, Authenticode. No home-grown handshake anywhere on the wire.
Desired state is integrity-checked
Every desired-state response carries an HMAC-SHA-256 keyed by that check-in’s one-time session secret. The agent verifies it and fails closed by default, so a tampered or injected response cannot add a package or arm a repair.
Installers are pinned
Every winget install is pinned to a SHA-256 the platform recorded: the vendor installer’s from ingest, or a derived MSI’s. Bytes your server keeps come from your own feed; anything else is the vendor’s copy and has to match the same hash. Chocolatey and Scoop packages are not served until they can be attested.
The shim is checked before it runs
The agent validates the shim’s Authenticode signature at start-up and refuses to run it otherwise. The shim itself is Apache-2.0 and has no network code at all.
Brokers prove who they are
A broker enrolls once with a token, then connects with a client certificate from the platform’s own broker CA, and pins the platform’s broker CA in return. The Proxmox host is treated as untrusted: before each clone the broker checks a signed template record and VM id it holds itself, though it cannot re-hash the template disk.
The database binds the platform too
Since platform 0.241.0, Community Edition connects to Postgres as a restricted role rather than as the database owner, so row-level security and the audit log’s insert-only rules apply to the platform itself. The reference compose creates the role; an install from before 0.241.0 moves to it once, by a runbook. A database superuser can still lift all of it, and the platform warns about a superuser connection rather than refusing one.
The installers are signed
Since platform 0.237.0 the agent and broker MSIs, the programs inside them and the wep-bundle-verify tool are Authenticode-signed with a Certum certificate and timestamped. Since 0.241.0 each release is checked on a clean machine before it is published, and fails if a signature does not verify.
The image is signed
Release images are cosign-signed and ship a CycloneDX SBOM, so you can verify what you pulled before you run it.
Audit spans are signed
Every audit span is signed with ECDSA P-256. The platform signs its own with a key your instance holds: a file on the data volume in Community Edition, a Key Vault in your Azure subscription on commercial tiers. From agent 0.240.0, each endpoint agent signs its spans with its own key, enrolled with the platform: in the TPM where the machine has a usable one, otherwise in the Windows software key store, from which a local administrator can extract it; the platform verifies them at ingest and again at export, and refuses an export window holding development-signed spans it cannot verify, from agents before 0.240.0 or one that could not create a key.
The full trust model, including what is still being built: /trust
The connections we left out on purpose.
A security tool earns the right to run on your fleet partly by what it does not do. Verify it yourself: deny all egress, log what is attempted, and run a full cycle. Any destination not on this page is a bug we want to hear about.
- No telemetry
- No usage analytics, no crash reporting, no activation call. Commercial licences verify offline against a key in the product. There is no code path to firewall.
- No inbound to endpoints
- Nothing listens on a managed machine. The agent asks; the platform answers.
- No revocation fetches from the server or broker
- Those chains validate against pinned roots. On endpoints, Windows checks revocation for the signatures on our own binaries.
- No NTP client
- Time comes from the host, as it does for everything else on it.
- No repair-time downloads
- The VC++ runtime the winget repair needs ships inside the agent MSI.
- No cloud unless you pick it
- Azure endpoints appear only when you configure an Azure backend. Community Edition dials none.
What works with the cable pulled, stated plainly.
- Inventory: winget, Chocolatey and plain uninstall entries.
- Reconciliation and deployment of versions detonated with their installer bytes kept on your server.
- Ring-staged agent updates with rollback.
- The signed audit log and offline-verifiable evidence export.
- CVE matching, from an offline bundle you import (signature-checked if you pin a key).
- Ingesting a new package or version. Manifests and installers come from the internet, and ingest is fail-closed without them.
- Installing a version whose bytes your server does not hold, since winget would fetch it from the vendor.
- There is no offline ingest path today.
- Freshness, drift and upstream-withdrawal monitoring, which are off by default anyway.
The middle ground: let the server reach the ingest hosts, keep endpoints off the internet, and promote only versions whose installer bytes your server keeps, so the server is the one host an endpoint needs for its installs.
A different server side, in your Azure subscription.
The Business tier is a dedicated instance we deploy and upgrade in your own Azure subscription. The endpoints, the agent and the detonation side do not change. The server side does: Container Apps behind a managed-certificate ingress, a private network, managed identities instead of passwords, and a deploy identity you own. Its full map, what you provide and what you grant us are on the dedicated instance page.
- Runs on
- Your Docker host
- Azure Container Apps in your subscription
- Database
- Postgres 18 sidecar
- Azure Database for PostgreSQL 18, no public access
- Installer bytes
- Data volume
- Storage account behind a private endpoint
- Image
- Pulled from ghcr.io
- Imported by digest into your own registry
- Sign-in
- Local accounts, roles, TOTP
- Entra ID SSO
- Audit signing key
- File on the data volume
- Key Vault in your subscription
- Broker port
- host :18443
- :8443, closed until you list your broker ranges
- Extra egress
- None
- Entra sign-in and the instance’s own Azure services; Microsoft Graph only if you turn on strict role checks
- Who upgrades
- You
- Attestree
One server. No open ports on your fleet.
Free Community Edition — self-host up to 200 endpoints, no Azure.