Kevinkv/#
Security and access

Every denial names the rule that caused it.

Kevin's trust layer is four things: rule-based entitlements over addressable resources, one sealed credential file instead of secrets hand-copied between machines, records that name the build that wrote them, and a WireGuard overlay with a kill switch that holds locally. All of it runs on your hardware.

does work stateful data third party initiates — this page is drawn in the platform's own diagram grammar

Entitlements: one question, one answer, one rule id

Every access decision in the platform reduces to the same triple: can this subject perform this operation on this resource. Resources are addressed as URIs, subjects as patterns, and the engine returns both the verdict and the identifier of the rule that produced it. A denial you cannot trace to a rule is a denial you cannot fix or audit, so the rule id travels with the 403. One engine answers this for HTTP routes, bus topics, vault documents and devices alike (it is called Grant in the source).

Caller

A tool, a browser session, a peer machine, or a daemon service.

Subject resolution

Bearer session token, then JWT, then a signed service header. Loopback falls back to a configured local user.

grant/__init__.py::get_subject

Policy evaluation

Matches every loaded rule on resource, subject and operation. Explicit deny wins over explicit allow, whatever the order.

grant/engine.py::evaluate

Policy files

JSON on disk, watched by OS filesystem events. An edit reloads the domain and clears the decision cache.

grant/policies/*.json

Decision

Allow is cached 30 seconds per triple. Deny is never cached, so a denial is re-evaluated every time it is asked.

Grant-Denied-Rule: <rule id>

Audit log

Append-only JSONL, one file per UTC day. Every denial is written. Successful checks are sampled.

grant/audit.py

Resources are addressed, not enumerated

Scheme selects the policy domain. Each domain declares its own default.

SchemeAddressesDefault
vault://Documents and objects in the vault.deny
api://Daemon HTTP routes.deny
mx://Device and firmware resources.deny
mqtt://Bus topics.allow
fed://Reserved for federation. No policy file ships, so it denies everything.n/a

The bus domain defaults to allow and the others default to deny. That is a deliberate asymmetry, and it is the first thing to change if your threat model includes an untrusted process on the same host. A resource whose scheme has no loaded policy is denied outright rather than guessed at.

A rule, verbatim

From the shipped vault domain. Patterns: * is one segment, ** is any.

// an explicit deny that no allow can override
{
  "id":       "vault-noted-export-deny",
  "resource": "vault://noted/**",
  "subjects": ["*"],
  "ops":      ["export"],
  "effect":   "deny"
}

Deny-beats-allow is what makes a blanket restriction survive later edits. A rule added next quarter that grants a team broad read access cannot silently re-open an export path closed here.

What the audit log is, and is not

It is a complete record of denials and of grants issued with a TTL. It is not a complete record of successful access: allows are sampled at a configurable rate, defaulting to one in ten thousand. If you need every successful read logged, raise the sample rate and size the disk for it. We would rather state the default than let you assume the stronger thing.

Two behaviours worth knowing before you deploy. The engine fails open during the boot window, before it has loaded policy, on the assumption that no external traffic reaches protected routes that early. And an unauthenticated request from loopback resolves to a configured local user rather than the anonymous principal, which means local access to the host is trusted by default. Both are defensible on a single owned machine and both are worth revisiting on a shared one.

Credentials live in one sealed file, never in a copied .env

The common failure is not weak encryption. It is a provider key that exists in nine places: a .env on the workstation, a second .env on the laptop, a CI secret, a password manager note, and a Slack message from the day it was rotated. Nobody can answer which copy is current, and rotation means finding all nine. Kevin collapses that to one encrypted file per machine, with the schema in version control and the values never in it.

Slot schema

Committed to git. Declares which providers exist and what field holds the secret. Values are empty placeholders.

daemon/config/vault-template.yaml

Bootstrap

On first run, seeds each slot from legacy sources in priority order, then stops consulting them. Idempotent.

daemon/src/bootstrap_vault.py

Sealed vault

One Fernet-encrypted JSON file, mode 0600. After bootstrap this is the only source of truth.

~/.kevin/vault.fernet

Why existing code keeps working

Provider SDKs read environment variables and will not be rewritten. At startup the vault exports its values into the process environment under the canonical names, and it overwrites what is already there rather than deferring to it. The direction matters: a stale shell variable cannot win against the vault, so there is exactly one answer to "which key is this process using".

sealed_vault.py::hydrate_env

One vocabulary table

Adding a provider means adding a slot to the template once. The credential registry, the environment-variable aliases, and the connections card in the operator UI are all derived from that file at load time rather than maintained as three tables that drift apart. Listings redact by default: first four characters, last four, nothing between.

The honest limit of this design

The master key is a file on the same disk as the vault it opens, protected by file permissions and nothing else. There is no passphrase prompt, no HSM, no cloud KMS, and no split-knowledge scheme. An attacker with file-level read access to that home directory has the vault. This is a meaningful improvement over secrets scattered across nine locations, and it is not equivalent to a managed secrets service with hardware-backed keys. If your control requires the latter, this is not it.

Records that name the build that wrote them

A running service can report its own version. That says nothing about rows already on disk. When a bug turns out to have been writing wrong values for six weeks, the question is which rows came from the broken build, and without provenance stored beside the data that question is archaeology rather than a query. The platform standard for this is S012: every record carries the build version and the logical service that wrote it.

Provenance on a stored record

The operation ledger stamps each row on insert. Column shape, not live data.

// daemon/src/operation_ledger.py
pub_version  TEXT   // build version of the writer
pub_service  TEXT   // logical service, a role

The service name is a role, not a machine name, so moving a workload to another host does not invalidate the history. The standard also defines a consumer half, recording which build read a record; this store implements the writer half only.

Provenance on a bus message

Stamped at publish, in a reserved envelope key.

"_meta": {
  "schema_version": 1,
  "pub_version":    "<build>",
  "pub_service":    "daemon"
}

Provenance lives in the envelope and is never merged into the payload, so a domain field can never collide with it. A message that fails its declared contract is not stamped even when enforcement is set to warn: stamping something just declared invalid would assert a conformance that is not true.

This is a retrofit in progress

Provenance is mandatory for new stores and is being retrofitted into existing ones. It is live in the operation ledger, in the tick cache, and on contracted bus topics. It is not yet on every table the platform writes, and the cost ledger is among those still being migrated. So: this is a standard we enforce on new work and a migration we are partway through, not a property you can assume of every row in the system today. Ask which stores carry it before you depend on it for a specific question.

Fleet and partner access over a WireGuard overlay

Machines reach each other over WireGuard. We did not write the cryptography and do not claim to have improved on it: it is the standard tunnel, with peers declared in a registry that lives in configuration. What the platform adds is honest state reporting and a kill switch that does not depend on the network it is switching off.

WireGuard

Third-party, unmodified. The tunnel is a service on the host; keys are generated and held client-side.

Three sources, never conflated

Overlay state reads the tunnel service state, the live interface dump with real peers and last-handshake ages, and the declared peer registry, and reports them separately. "Service running" is not "peers reachable", and collapsing the two is how a dead link reads as healthy.

daemon/src/fleet_net/overlay.py

Kill switch

Authorized through the entitlement engine, published on the bus before it is enacted so the fleet hears the intent even if local enactment then fails, and enforced locally by holding the tunnel service down. It persists across restarts: a kill survives a reboot until explicitly re-armed.

daemon/src/fleet_net/killswitch.py

The switch never touches loopback. It kills the overlay, not the interface the operator is sitting on, because a remote control plane that can strand you is worse than no remote control plane.

One thing this is not yet. Partner access today is network-level, meaning the tunnel plus the peer registry plus the same route rules that apply to any subject. Per-partner federation policy has a reserved address scheme and no shipped policy file, so that scheme currently denies everything. If you need contractual data-sharing boundaries expressed as policy rather than as network reachability, that work is ahead of us, not behind us.

Where this is the wrong choice

A security page that only lists strengths tells a technical reader that the omissions are the interesting part. These are the cases where you should choose something else.

This layer earns its place when…

  • Data cannot leave your building, and a vendor control plane in the request path is itself the problem.
  • You need to answer "who could have read this, and who was refused" from a file you own.
  • Credentials are currently spread across machines by hand and nobody can say which copy is live.
  • You operate a small fleet and need one authorization model across services, bus topics and devices rather than four.
  • You want the ability to sever fleet connectivity from a control node and have it hold locally.

Choose something else when…

  • Your procurement requires SOC 2, a HIPAA BAA, or FedRAMP. Kevin has none of them. This is the disqualifying case and it is not close.
  • Your security review is a vendor risk review. We are a small vendor. Against a balance sheet that argument is lost on the first slide, and the honest counter is only that the deployment runs on your hardware and outlives us.
  • You require hardware-backed key custody. The vault master key is a permissioned file, not an HSM.
  • You need every successful access logged by default. Denials are complete; allows are sampled unless you raise the rate.
  • You need per-partner data-sharing policy today. The federation scheme is reserved and unshipped.