This commit is contained in:
+137
-172
@@ -1,184 +1,149 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Platform",
|
||||
description:
|
||||
"How Vantage fits together: a control plane you run, one agent per server, and a single outbound connection between them.",
|
||||
title: "Platform",
|
||||
description: "How Vantage fits together: a control plane you run, one agent per server, and a single outbound connection between them.",
|
||||
};
|
||||
|
||||
export default function PlatformPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="rail band band--open">
|
||||
<span className="tag">Platform</span>
|
||||
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "19ch" }}>
|
||||
How the pieces fit together.
|
||||
</h1>
|
||||
<p className="lede">
|
||||
Three moving parts: a control plane you run, an agent on each server, and one outbound connection between
|
||||
them.
|
||||
</p>
|
||||
return (
|
||||
<>
|
||||
<section className="rail band band--open">
|
||||
<span className="tag">Platform</span>
|
||||
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "19ch" }}>How the pieces fit together.</h1>
|
||||
<p className="lede">Three moving parts: a control plane you run, an agent on each server, and one outbound connection between them.</p>
|
||||
|
||||
<div className="split" style={{ marginTop: "3rem" }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: "var(--s-2)", maxWidth: "18ch" }}>The agent never listens.</h2>
|
||||
<div className="prose" style={{ marginTop: "1rem" }}>
|
||||
<p>
|
||||
Every agent dials out to the control plane over gRPC with TLS. Nothing needs an inbound port, nothing
|
||||
needs a static address, and a machine behind NAT is no different from one with a public IP.
|
||||
</p>
|
||||
<p>
|
||||
Key state is polled on a 30-second interval, because 30 seconds is fine for access control and polling
|
||||
is simple to reason about. Everything that should not wait running a step, opening a console, applying
|
||||
updates is pushed down a bidirectional command stream the agent holds open.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="split" style={{ marginTop: "3rem" }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: "var(--s-2)", maxWidth: "18ch" }}>The agent never listens.</h2>
|
||||
<div className="prose" style={{ marginTop: "1rem" }}>
|
||||
<p>
|
||||
Every agent dials out to the control plane over gRPC with TLS. Nothing needs an inbound port, nothing needs a static address, and a machine behind NAT is no different
|
||||
from one with a public IP.
|
||||
</p>
|
||||
<p>
|
||||
Key state is polled on a 30-second interval, because 30 seconds is fine for access control and polling is simple to reason about. Everything that should not wait
|
||||
running a step, opening a console, applying updates is pushed down a bidirectional command stream the agent holds open.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="specs specs--flush">
|
||||
<div className="spec">
|
||||
<span className="spec__k">POLL</span>
|
||||
<div>
|
||||
<h3>SyncKeys, every 30s</h3>
|
||||
<p>The desired key set for this server. Unchanged state means no disk write at all.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">PUSH</span>
|
||||
<div>
|
||||
<h3>Command stream</h3>
|
||||
<p>Generate a key, run a step, apply updates, update the agent, clean up a workspace.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">REPORT</span>
|
||||
<div>
|
||||
<h3>Inventory and checks</h3>
|
||||
<p>
|
||||
Metrics every 30 seconds, a full hardware snapshot every 15 minutes, and monitor results as they
|
||||
complete.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="specs specs--flush">
|
||||
<div className="spec">
|
||||
<span className="spec__k">POLL</span>
|
||||
<div>
|
||||
<h3>SyncKeys, every 30s</h3>
|
||||
<p>The desired key set for this server. Unchanged state means no disk write at all.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">PUSH</span>
|
||||
<div>
|
||||
<h3>Command stream</h3>
|
||||
<p>Generate a key, run a step, apply updates, update the agent, clean up a workspace.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">REPORT</span>
|
||||
<div>
|
||||
<h3>Inventory and checks</h3>
|
||||
<p>Metrics every 30 seconds, a full hardware snapshot every 15 minutes, and monitor results as they complete.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rail band">
|
||||
<span className="tag">Write path</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "24ch" }}>
|
||||
The file is never half-written.
|
||||
</h2>
|
||||
<div className="split split--even" style={{ marginTop: "2rem" }}>
|
||||
<p className="prose">
|
||||
The agent computes the desired <code>authorized_keys</code> content, compares it to what is on disk, and
|
||||
stops there if nothing changed. When it does need to write, it writes a temporary file in the same directory
|
||||
and renames it over the real one. A machine that loses power mid-write keeps the file it had.
|
||||
</p>
|
||||
<pre className="code">
|
||||
<i>
|
||||
{"\n"}
|
||||
desired := client.SyncKeys(serverID, token){"\n"}
|
||||
current := keys.ReadAuthorizedKeys(){"\n\n"}
|
||||
<b>if</b> !keys.StateChanged(current, desired) {"{"}
|
||||
{"\n "}
|
||||
<i>
|
||||
{"\n "}
|
||||
<b>return</b> nil{"\n"}
|
||||
{"}"}
|
||||
{"\n\n"}
|
||||
keys.WriteAuthorizedKeys(desired){"\n"}
|
||||
<i>
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
<section className="rail band">
|
||||
<span className="tag">Write path</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "24ch" }}>The file is never half-written.</h2>
|
||||
<div className="split split--even" style={{ marginTop: "2rem" }}>
|
||||
<p className="prose">
|
||||
The agent computes the desired <code>authorized_keys</code> content, compares it to what is on disk, and stops there if nothing changed. When it does need to write, it writes a
|
||||
temporary file in the same directory and renames it over the real one. A machine that loses power mid-write keeps the file it had.
|
||||
</p>
|
||||
<pre className="code">
|
||||
<i>// agent poll, simplified</i>
|
||||
{"\n"}
|
||||
desired := client.SyncKeys(serverID, token){"\n"}
|
||||
current := keys.ReadAuthorizedKeys(){"\n\n"}
|
||||
<b>if</b> !keys.StateChanged(current, desired) {"{"}
|
||||
{"\n "}
|
||||
<i>// nothing to do</i>
|
||||
{"\n "}
|
||||
<b>return</b> nil{"\n"}
|
||||
{"}"}
|
||||
{"\n\n"}
|
||||
keys.WriteAuthorizedKeys(desired){"\n"}
|
||||
<i>// write .tmp, os.Rename(), chmod 0600</i>
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rail band">
|
||||
<span className="tag">Tenancy and identity</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>
|
||||
Organisations are the boundary.
|
||||
</h2>
|
||||
<div className="caps">
|
||||
<article className="cap">
|
||||
<span className="cap__k">Isolation</span>
|
||||
<h3>Scoped at the query</h3>
|
||||
<p>
|
||||
Every server, key, workflow, monitor and secret belongs to an organisation, and every lookup is filtered
|
||||
by it. Uniqueness constraints are enforced by the database, not by application logic.
|
||||
</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Roles</span>
|
||||
<h3>Owner, admin, member</h3>
|
||||
<p>
|
||||
Members operate the fleet. Admins and owners manage people, identity settings and the secrets read token.
|
||||
</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Identity</span>
|
||||
<h3>Local or OIDC, per organisation</h3>
|
||||
<p>
|
||||
Sign in with email and password, or connect your own provider. Each organisation configures its own issuer
|
||||
and client.
|
||||
</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Sessions</span>
|
||||
<h3>Server-side, 24 hours</h3>
|
||||
<p>
|
||||
Cookies carry an opaque identifier and nothing else. Session bodies live in Redis, so losing it signs
|
||||
everyone out and costs no durable data.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section className="rail band">
|
||||
<span className="tag">Tenancy and identity</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>Organisations are the boundary.</h2>
|
||||
<div className="caps">
|
||||
<article className="cap">
|
||||
<span className="cap__k">Isolation</span>
|
||||
<h3>Scoped at the query</h3>
|
||||
<p>
|
||||
Every server, key, workflow, monitor and secret belongs to an organisation, and every lookup is filtered by it. Uniqueness constraints are enforced by the database, not by
|
||||
application logic.
|
||||
</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Roles</span>
|
||||
<h3>Owner, admin, member</h3>
|
||||
<p>Members operate the fleet. Admins and owners manage people, identity settings and the secrets read token.</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Identity</span>
|
||||
<h3>Local or OIDC, per organisation</h3>
|
||||
<p>Sign in with email and password, or connect your own provider. Each organisation configures its own issuer and client.</p>
|
||||
</article>
|
||||
<article className="cap">
|
||||
<span className="cap__k">Sessions</span>
|
||||
<h3>Server-side, 24 hours</h3>
|
||||
<p>Cookies carry an opaque identifier and nothing else. Session bodies live in Redis, so losing it signs everyone out and costs no durable data.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rail band">
|
||||
<span className="tag">What we do not build</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>The scope is the feature.</h2>
|
||||
<div className="specs" style={{ maxWidth: "70ch" }}>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NOT A PROXY</span>
|
||||
<div>
|
||||
<h3>We are never in the SSH path</h3>
|
||||
<p>
|
||||
Vantage assigns keys; your client connects straight to the box. If our control plane is down, your SSH
|
||||
still works.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO CUSTODY</span>
|
||||
<div>
|
||||
<h3>Private keys stay put by default</h3>
|
||||
<p>
|
||||
Keys generated on a server stay on it unless you explicitly upload the private half, and anything stored
|
||||
is encrypted with a key only your deployment holds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO PER-USER</span>
|
||||
<div>
|
||||
<h3>Root, not every account</h3>
|
||||
<p>
|
||||
Vantage manages one file per server. Per-user key management is a different product with a different
|
||||
failure mode.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO PLUGINS</span>
|
||||
<div>
|
||||
<h3>An agent you can read in an evening</h3>
|
||||
<p>
|
||||
A few thousand lines of Go with no extension system. Auditability beats extensibility on a binary that
|
||||
runs as root.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
<section className="rail band">
|
||||
<span className="tag">What we do not build</span>
|
||||
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>The scope is the feature.</h2>
|
||||
<div className="specs" style={{ maxWidth: "70ch" }}>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NOT A PROXY</span>
|
||||
<div>
|
||||
<h3>We are never in the SSH path</h3>
|
||||
<p>Vantage assigns keys; your client connects straight to the box. If our control plane is down, your SSH still works.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO CUSTODY</span>
|
||||
<div>
|
||||
<h3>Private keys stay put by default</h3>
|
||||
<p>Keys generated on a server stay on it unless you explicitly upload the private half, and anything stored is encrypted with a key only your deployment holds.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO PER-USER</span>
|
||||
<div>
|
||||
<h3>Root, not every account</h3>
|
||||
<p>Vantage manages one file per server. Per-user key management is a different product with a different failure mode.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">NO PLUGINS</span>
|
||||
<div>
|
||||
<h3>An agent you can read in an evening</h3>
|
||||
<p>A few thousand lines of Go with no extension system. Auditability beats extensibility on a binary that runs as root.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user