Skip to Content
Energon runs in the operator's Cloudflare account. Published links are open by default.
Security and SharingSecurity model

Use four separate controls to decide who signs in, calls the API, opens a published link, and changes its bytes. This model is for publishers, operators, and reviewers assessing an Energon handoff. Keep the controls separate because a share password or creator-only write policy never substitutes for reader-specific authorization.

Security model

Map each request to its control

Start by identifying the caller and surface:

QuestionControlWhat it protects
Who may use the human hub?Cloudflare AccessHub, token minting, and account actions
Who may call the agent API?API token sent as a bearer credential/v1 reads and writes
Who may open this public content URL?Optional share passwordOne site’s or file’s public content URL
Who may change this object?write_policyPUT, PATCH, and DELETE mutations

Cloudflare Access authenticates humans on the hub origin. Agents send API tokens with Authorization: Bearer, so /v1 remains reachable without an Access cookie. Published links live on the separate content origin and are public by default. The host split also keeps uploaded HTML and SVG away from the authenticated hub origin.

The trust boundaries

Human identity

In production, Energon accepts a human identity only from Cloudflare’s verified Access runtime. It requires an email and a stable identity-provider subject, then applies ALLOWED_EMAIL_DOMAINS. The stable subject matters: if an email address is later reused by a different identity, the replacement does not inherit owner-only objects or old tokens.

Local development is intentionally different. On localhost or 127.0.0.1, Access is skipped and identity comes from DEV_ACCESS_EMAIL, an Access-like test header, or dev@example.com. Never use that local behavior as evidence that production trusts client-provided Access headers.

Agent identity

A signed-in human creates an API token at /tokens. The token secret is shown once. Energon stores its hash and a masked hint, not a recoverable secret. Every authenticated /v1 request becomes the token owner’s actor, so a leaked token carries that account’s instance-wide read privileges and whatever mutation privileges each object permits.

Check a configured agent without exposing the token:

export ENERGON_ORIGIN="https://energon.company.example" # ENERGON_TOKEN must be the real secret a human minted at $ENERGON_ORIGIN/tokens. test -n "$ENERGON_TOKEN" || { echo "Set ENERGON_TOKEN first" >&2; exit 1; } curl -sS "$ENERGON_ORIGIN/v1/whoami" \ -H "Authorization: Bearer $ENERGON_TOKEN"

The response identifies the owning email, token label, and expires_at. A 401 with error: token_expired is terminal: tokens cannot be extended, so a human must mint a replacement.

Public reader

Anyone with an unprotected public content URL can open it. A share password adds a gate to that URL for browsers and unauthenticated agents. It does not change /v1: a valid API token can read the bytes without the share password.

Writer

Each site and loose file stores one write policy:

  • owner: only the creating account may PUT, PATCH, or DELETE.
  • instance: any valid API token on this Energon instance may mutate it.

Only the creator can change the policy. Neither value changes read access. A valid API token can still read or duplicate an owner-only object; the duplicate becomes a new object owned by the duplicating account.

Defense around published content

Energon serves production content from CONTENT_ORIGIN, separate from PUBLIC_ORIGIN. Requests for content on the hub redirect to the content hostname, while account routes are not served there. Published HTML, XHTML, and SVG receive a sandbox Content Security Policy without allow-same-origin. Rendered Markdown is sanitized, and password-protected responses are private and not stored in the shared cache.

These protections reduce what active uploads can do, but they do not make an open link private. Choose a share password for link-level access and an appropriate TTL for how long the object should exist.

Continue reading

Last updated on