Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82ef84aa49 |
@@ -1,127 +0,0 @@
|
||||
// cloudflare-dns's own code (novox/hq ADR 0039). It provides the mesh `public-dns` interface
|
||||
// (ADR 0044): a public name that resolves to the mesh's public ingress. Cloudflare is one registrar
|
||||
// behind the neutral interface — a consumer names `public-dns`, never Cloudflare — so this file is
|
||||
// the only place Cloudflare's API appears, and swapping registrars swaps only this module.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface PublicRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class CloudflareClient {
|
||||
constructor(
|
||||
private readonly token: string,
|
||||
private readonly zoneId: string,
|
||||
/** The zone this registers under, e.g. "example.com". */
|
||||
readonly domain: string,
|
||||
/** What every public name points at — the mesh's public ingress (the reverse proxy). */
|
||||
readonly ingress: string,
|
||||
) {}
|
||||
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): CloudflareClient {
|
||||
// Which zone, domain and ingress are a mesh's own facts, not this module's — so they are
|
||||
// settings, merged into a config file the mesh manages (novox/hq ADR 0046), read here. The
|
||||
// token is the one secret and stays an own-secret. Env is honoured as a fallback for a
|
||||
// hand-run instance, but the deployed path is the config file settings fill.
|
||||
const config = readConfig(env.MESH_CLOUDFLARE_CONFIG_FILE);
|
||||
const token = env.MESH_CLOUDFLARE_TOKEN ?? readSecret(env.MESH_CLOUDFLARE_TOKEN_FILE);
|
||||
const zoneId = config.zone ?? env.MESH_CLOUDFLARE_ZONE_ID;
|
||||
const domain = config.domain ?? env.MESH_PUBLIC_DOMAIN;
|
||||
const ingress = config.ingress ?? env.MESH_PUBLIC_INGRESS;
|
||||
if (!token || !zoneId || !domain || !ingress) {
|
||||
throw new Error(
|
||||
"cloudflare-dns is not configured — set its zone, domain and ingress in settings (and the " +
|
||||
"token as its own-secret); until then it registers nothing",
|
||||
);
|
||||
}
|
||||
return new CloudflareClient(token, zoneId, domain, ingress);
|
||||
}
|
||||
|
||||
/**
|
||||
* The public name a consumer gets: derived from its identity under the mesh's domain. Derived, not
|
||||
* contributed, for the same reason minio derives a bucket name — the harness hands `remove` only
|
||||
* the identity, so teardown must recompute exactly what creation made.
|
||||
*/
|
||||
nameFor(consumer: string): string {
|
||||
return `${consumer.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase()}.${this.domain}`;
|
||||
}
|
||||
|
||||
/** An IP points at itself (A/AAAA); a hostname points through a CNAME. */
|
||||
private recordType(): "A" | "AAAA" | "CNAME" {
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(this.ingress)) return "A";
|
||||
if (this.ingress.includes(":")) return "AAAA";
|
||||
return "CNAME";
|
||||
}
|
||||
|
||||
private async api<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
|
||||
method,
|
||||
headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
const json = (await res.json()) as { success?: boolean; result?: unknown; errors?: unknown };
|
||||
if (!res.ok || json.success === false) {
|
||||
throw new Error(`cloudflare ${method} ${path}: ${res.status} ${JSON.stringify(json.errors ?? json)}`);
|
||||
}
|
||||
return json.result as T;
|
||||
}
|
||||
|
||||
async findRecord(name: string): Promise<PublicRecord | undefined> {
|
||||
const records = await this.api<PublicRecord[]>(
|
||||
"GET",
|
||||
`/zones/${this.zoneId}/dns_records?name=${encodeURIComponent(name)}`,
|
||||
);
|
||||
return records[0];
|
||||
}
|
||||
|
||||
/** Point a public name at the mesh's ingress, idempotently — create it, or update one already there. */
|
||||
async upsert(name: string): Promise<PublicRecord> {
|
||||
const body = { type: this.recordType(), name, content: this.ingress, ttl: 300, proxied: false };
|
||||
const existing = await this.findRecord(name);
|
||||
if (existing) {
|
||||
return this.api<PublicRecord>("PUT", `/zones/${this.zoneId}/dns_records/${existing.id}`, body);
|
||||
}
|
||||
return this.api<PublicRecord>("POST", `/zones/${this.zoneId}/dns_records`, body);
|
||||
}
|
||||
|
||||
/** Remove a public name, idempotently — a record already gone is not an error on reconcile. */
|
||||
async remove(name: string): Promise<void> {
|
||||
const existing = await this.findRecord(name);
|
||||
if (existing) await this.api("DELETE", `/zones/${this.zoneId}/dns_records/${existing.id}`);
|
||||
}
|
||||
|
||||
/** Every record in the zone, for the diagnostic tool. */
|
||||
async records(): Promise<PublicRecord[]> {
|
||||
return this.api<PublicRecord[]>("GET", `/zones/${this.zoneId}/dns_records`);
|
||||
}
|
||||
}
|
||||
|
||||
function readSecret(path: string | undefined): string | undefined {
|
||||
if (!path) return undefined;
|
||||
try {
|
||||
return readFileSync(path, "utf8").trim();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface Config {
|
||||
zone?: string;
|
||||
domain?: string;
|
||||
ingress?: string;
|
||||
}
|
||||
|
||||
/** The settings-managed config file (a JSON document the mesh merges settings into). Absent or
|
||||
* unparseable yields an empty config, which fromEnv then reports as unconfigured. */
|
||||
function readConfig(path: string | undefined): Config {
|
||||
if (!path) return {};
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8")) as Config;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"module": "cloudflare-dns",
|
||||
"version": "1",
|
||||
"slug": "cfdns",
|
||||
"provides": [
|
||||
{
|
||||
"name": "public-dns",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"serves": {
|
||||
"public-dns": {}
|
||||
},
|
||||
"grants": {
|
||||
"public-dns": "${dir:grants}"
|
||||
},
|
||||
"receives": {
|
||||
"public-dns": "${dir:grants}/mesh.json"
|
||||
},
|
||||
"own-secrets": {
|
||||
"token": "${dir:state}/token"
|
||||
},
|
||||
"emits": [
|
||||
"record.created",
|
||||
"record.removed"
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "grants",
|
||||
"type": "directory",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "${dir:state}/config.json",
|
||||
"merge": "json",
|
||||
"content": "{}",
|
||||
"mode": "0600"
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "code",
|
||||
"kind": "bundle",
|
||||
"language": "typescript",
|
||||
"entrypoints": [
|
||||
"tools/index.js",
|
||||
"provisioner/index.js"
|
||||
],
|
||||
"loads": [
|
||||
"tools/index.js",
|
||||
"provisioner/index.js"
|
||||
],
|
||||
"env": {
|
||||
"MESH_CLOUDFLARE_TOKEN_FILE": "${dir:state}/token",
|
||||
"MESH_CLOUDFLARE_CONFIG_FILE": "${dir:state}/config.json",
|
||||
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "@novox/module-cloudflare-dns",
|
||||
"version": "0.1.0",
|
||||
"description": "cloudflare-dns — a public-dns provider (ADR 0044): registers public names at Cloudflare.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@novox/mesh-sdk": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// cloudflare-dns's provisioner — the adapter making it a provider of the mesh `public-dns` interface
|
||||
// (novox/hq ADR 0044). The reconcile loop and the contributions file are the sdk harness's; this
|
||||
// writes only the per-registrar half: register a consumer's public name at Cloudflare, pointing it
|
||||
// at the mesh's ingress, and remove it when the consumer is withdrawn (ADR 0048).
|
||||
//
|
||||
// The `public-dns` interface hands a consumer { fqdn, target, ttl } — a name that resolves publicly
|
||||
// and what it resolves to. Like umami's analytics it is a *data* provision, not a credential one:
|
||||
// nothing the mesh mints is set here (a DNS record is public, and the only secret is this module's
|
||||
// own Cloudflare token, which never leaves). So the password the harness carries is unused; the name
|
||||
// is derived from the login the mesh gave the consumer, which the consumer can derive too. Delivering
|
||||
// the record back to the consumer is the data-provision return path ADR 0048 leaves out of scope.
|
||||
|
||||
import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner";
|
||||
import { emit } from "@novox/mesh-sdk/events";
|
||||
import { CloudflareClient } from "../client.js";
|
||||
|
||||
const cloudflare = CloudflareClient.fromEnv();
|
||||
|
||||
runProvisioner("public-dns", {
|
||||
async create(p: Provision): Promise<void> {
|
||||
const fqdn = cloudflare.nameFor(p.as);
|
||||
await cloudflare.upsert(fqdn);
|
||||
await announce("record.created", {
|
||||
name: fqdn,
|
||||
target: cloudflare.ingress,
|
||||
consumer: p.consumer ?? "",
|
||||
});
|
||||
},
|
||||
|
||||
async remove(p: { as: string }): Promise<void> {
|
||||
const fqdn = cloudflare.nameFor(p.as);
|
||||
await cloudflare.remove(fqdn);
|
||||
await announce("record.removed", { name: fqdn, consumer: p.as });
|
||||
},
|
||||
});
|
||||
|
||||
/** Emit best-effort: a broker hiccup must never fail or reverse a DNS change that already happened. */
|
||||
async function announce(type: string, body: unknown): Promise<void> {
|
||||
try {
|
||||
await emit(type, body);
|
||||
} catch (err) {
|
||||
console.error(`[cloudflare-dns] could not emit ${type}: ${err}`);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// cloudflare-dns's tool — the diagnostic: what public names the mesh currently publishes here.
|
||||
|
||||
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import { CloudflareClient } from "../client.js";
|
||||
|
||||
export function getCloudflareDnsTools(cloudflare: CloudflareClient): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: "cloudflare_dns_records",
|
||||
description: "The public DNS records in the mesh's zone — the names it currently publishes.",
|
||||
input: {},
|
||||
run: async () => ({ domain: cloudflare.domain, ingress: cloudflare.ingress, records: await cloudflare.records() }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
registerModuleTools("cloudflare-dns", (env) => {
|
||||
try {
|
||||
return getCloudflareDnsTools(CloudflareClient.fromEnv(env));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"client.ts",
|
||||
"tools/index.ts",
|
||||
"provisioner/index.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
# dhcpcd
|
||||
|
||||
The uplink seat's module for a machine whose own network is dhcpcd's (novox/hq ADR 0117). It
|
||||
asks two things of dhcpcd, and nothing else: leave the resolver file to the mesh, and leave the
|
||||
private network's interface alone — and it writes that resolver file itself (ADR 0223). It never
|
||||
declares an interface, an address, a route, a wireless network or its credentials — the link
|
||||
dhcpcd keeps is the only channel the mesh reaches the machine over.
|
||||
|
||||
## What it writes
|
||||
|
||||
`/etc/resolv.conf`, whole: every resolver of the mesh by its private address — this machine's own
|
||||
first when it holds one — and `options timeout:1 attempts:2 edns0`, rendered by the mesh from the
|
||||
holders of `mesh-dns-resolver`. The same file every uplink module writes. Not dhcpcd's own static
|
||||
`domain_name_servers`: dhcpcd writes the file only through its hook, with its own header, and reads
|
||||
its configuration only at its next start, so a change to the mesh's resolvers would not reach the
|
||||
file until then.
|
||||
|
||||
Two lines into `/etc/dhcpcd.conf`, as the mesh's marked region (`into: block`) — dhcpcd reads no
|
||||
drop-in directory, so the mesh writes into its one file rather than over it (ADR 0102):
|
||||
|
||||
- `nohook resolv.conf` — dhcpcd's resolv.conf hook rewrites `/etc/resolv.conf` on every lease it
|
||||
takes or renews, which would silently replace the resolvers this module writes there.
|
||||
- `denyinterfaces mesh0` — dhcpcd never asks for a lease on the private network's interface, and
|
||||
never takes it down. dhcpcd leaves a point-to-point interface alone by default; this says so
|
||||
rather than relying on it.
|
||||
|
||||
**At the start of the file** (`at: start`). Both are global options, and dhcpcd reads every line
|
||||
after an `interface` or `ssid` line as that interface's own. A configured machine's file ends in
|
||||
exactly such a block (the interface, its static address), so appended at the end these two would
|
||||
quietly apply to one interface only.
|
||||
|
||||
## Why it declares no service
|
||||
|
||||
dhcpcd is the machine's, not the mesh's. The mesh never starts, stops or enables it: stopping it
|
||||
drops the address the machine is reached at, and a module unassigned by mistake must not be able
|
||||
to do that. And there is nothing to reload it with — `dhcpcd.service` reports `CanReload=no`, and
|
||||
a restart drops the lease. So the two lines take effect at **dhcpcd's next start**.
|
||||
|
||||
On an adopted machine that is normally no gap: the predecessor wrote the same `nohook` line, and
|
||||
it is already in force. **On a machine that was not adopted, it is one:** until dhcpcd next
|
||||
starts (a reboot, or the operator restarting it in a window of their choosing), a lease renewal
|
||||
still rewrites `/etc/resolv.conf`, and this module puts it back at the next push. Restart dhcpcd
|
||||
once, by hand, when losing the link for a moment is acceptable.
|
||||
|
||||
## One manager per machine
|
||||
|
||||
It claims `node-uplink`: a machine runs one network manager, and assigning a second module that
|
||||
claims the seat is refused. Assigning this one to a machine whose network is NetworkManager's
|
||||
installs the package and writes the two lines, and starts nothing.
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"module": "dhcpcd",
|
||||
"version": "1",
|
||||
"requires": [
|
||||
"wildcard-resolution"
|
||||
],
|
||||
"capabilities": [
|
||||
"package-manager",
|
||||
"service-manager",
|
||||
"uplink-dhcpcd"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-uplink",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"facts": {
|
||||
"resolvers": {
|
||||
"path": "/etc/resolv.conf",
|
||||
"template": "# Managed by the mesh, and written by the module holding this machine's uplink:\n# the program that manages the machine's network would otherwise rewrite this\n# file on every change of network, so its holder is the one that writes it\n# (novox/hq ADR 0117, ADR 0223). Replaced on every push; edit nothing here.\n#\n# Every resolver of the mesh, by address, and nothing else (novox/hq ADR 0223) \u2014\n# this machine's own first when it holds one, then the others by name. Each\n# answers the mesh's names from the same roster and forwards every other name, so\n# whichever answers first gives the one answer. There is no public resolver here:\n# a C library that asks every listed server at once and takes the first reply \u2014\n# musl, so every Alpine container \u2014 took a public resolver's \"no such name\" for\n# a mesh name and failed. A machine that reaches none of these has no names until\n# it does. Containers copy these lines from their machine.\n{{range index .Holders \"mesh-dns-resolver\"}}nameserver {{.Address}}\n{{end}}options timeout:1 attempts:2 edns0\n"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "dhcpcd"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/etc/dhcpcd.conf",
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The mesh's two lines (module dhcpcd, novox/hq ADR 0117, ADR 0223): the\n# resolver file is this module's, written whole beside this file. Global\n# options, so kept above any interface line; read at dhcpcd's next start.\nnohook resolv.conf\ndenyinterfaces mesh0\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"module": "public-acme",
|
||||
"version": "1",
|
||||
"slug": "pubacme",
|
||||
"provides": [
|
||||
{
|
||||
"name": "acme-ca",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"serves": {
|
||||
"acme-ca": {
|
||||
"at": "acme-v02.api.letsencrypt.org",
|
||||
"port": 443,
|
||||
"path": "/directory",
|
||||
"roots": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ the same proxy, pointing at the mesh's container instead of the predecessor's.
|
||||
| `serves.route` | `{}` | a route hands back a name, not a credential |
|
||||
| `receives.route` | `…/routes/mesh.json` | the same contributions file, in the same shape |
|
||||
| `listens` | *nothing* | **the point.** It opens no port, so it stands beside the predecessor rather than replacing it |
|
||||
| `requires` | *nothing* | in particular not `acme-ca`: the predecessor holds the certificates and this must not ask for one |
|
||||
| `requires` | *nothing* | in particular no certificate issuer: the predecessor holds the certificates and this must not obtain one |
|
||||
|
||||
Assign **either** this or `route-proxy` to a node, never both — two providers of one mesh-scoped
|
||||
provision is an ambiguity the resolver is right to refuse.
|
||||
@@ -95,7 +95,7 @@ after the module was unassigned.
|
||||
|
||||
Where the predecessor keeps the directory its file provider reads is a fact about **one machine**,
|
||||
so it is a setting laid over the module's default rather than a constant in the catalogue —
|
||||
novox/hq ADR 0100's `ports` is the precedent, and cloudflare-dns's `config.json` is the mechanism:
|
||||
novox/hq ADR 0100's `ports` is the precedent, and the mechanism is
|
||||
a `merge: "json"` file the mesh composes from the module's defaults and the node's layer, mounted
|
||||
into the container.
|
||||
|
||||
@@ -141,7 +141,7 @@ published ports — is a decision for novox/hq, not for a module that is schedul
|
||||
## How it ships
|
||||
|
||||
The tool runtime carrying this module's compiled code, as
|
||||
[cloudflare-dns](../cloudflare-dns/Dockerfile) and [mosquitto](../mosquitto/Dockerfile) do — built
|
||||
[mosquitto](../mosquitto/Dockerfile) does — built
|
||||
from this directory and nothing else. It connects to no broker: reconciling files on the machine it
|
||||
runs on is an offline operation, and `mesh-tools run` gives it exactly that.
|
||||
|
||||
|
||||
@@ -23,20 +23,25 @@ it.
|
||||
| `receives.route` | `…/routes/mesh.json` | where the mesh writes every contribution; the proxy reads it as `$ROUTES` and re-reads on change |
|
||||
| `listens` | `80` and `443`, both `from: anywhere` | the one machine with a public opening; the firewall opens these for free (ADR 0045) |
|
||||
|
||||
No broker account, no own-secrets, no provisioner: the proxy neither mints a credential nor emits
|
||||
an event. It only reads the file the mesh writes. (Contrast `redis`, which mints passwords, and
|
||||
`cloudflare-dns`, which emits record events.)
|
||||
No provisioner: the proxy neither mints a credential for anybody nor answers a provision other
|
||||
than `route`. It reads the file the mesh writes, and its own broker credential is for its tools.
|
||||
|
||||
## Which issuer: the node that runs a proxy carries `public-acme`
|
||||
## Which issuer: Let's Encrypt, stated here (novox/hq ADR 0226)
|
||||
|
||||
`acme-ca` has two providers in a full mesh — `public-acme` (Let's Encrypt, a facts-only module that
|
||||
runs nothing) and `step-ca` (the mesh's own authority, which also offers it so a lab without a public
|
||||
issuer still has one). A proxy beside both resolves by co-location only once a pin names the module
|
||||
(`pin <node> acme-ca <node> public-acme`, novox/hq #258); a proxy on another machine cannot resolve
|
||||
at all until it is told. **So every node that runs a route-proxy is assigned `public-acme` too**: the
|
||||
issuer is then on the proxy's own node, design 23's first rule answers, and `internal-acme-ca` has
|
||||
one provider mesh-wide. Nothing runs for it; it is the statement "this machine's public issuer is
|
||||
Let's Encrypt", on the machine that issues.
|
||||
**The public issuer is this module's own fact, not a provision.** Until 2026-10-06 it came from a
|
||||
second module, `public-acme`, that ran nothing and offered `acme-ca` pointing at Let's Encrypt; every
|
||||
node running a proxy was assigned it too, and nothing else ever consumed it. It is retired: the proxy
|
||||
names the production directory in its own `acme.env`, and the only issuer it is still bound to is the
|
||||
mesh's own (`internal-acme-ca`, from `step-ca`).
|
||||
|
||||
**`acme.env` is byte for byte what the binding rendered.** The proxy keeps each authority's account
|
||||
and certificates in a directory under `/var/lib/route-proxy/acme` named after a digest of the
|
||||
directory URL — `https://acme-v02.api.letsencrypt.org:443/directory`, port included, as the binding
|
||||
spelled it — and of the root bundle the `trust` container copies in. Any change to either is a new
|
||||
authority to the proxy: a new account, and every routed name ordered again against Let's Encrypt's
|
||||
rate limits. So the URL keeps the `:443`, `ACME_ROOTS_PATH` stays empty, and the `trust` artifact's
|
||||
pinned image is not moved without a decision; mesh-controller's
|
||||
`TestRouteProxyKeepsItsPublicAccountDirectory` holds all three.
|
||||
|
||||
## How it ships the Go proxy
|
||||
|
||||
@@ -66,9 +71,10 @@ would leave the safe path depending on somebody remembering to opt out of it, on
|
||||
most likely to iterate. A staging certificate is trusted by no browser, so the mistake announces
|
||||
itself on the first request rather than a fortnight later at the rate limit.
|
||||
|
||||
**A node that serves real public traffic states so**, by overriding `ACME_DIRECTORY` to the
|
||||
production directory `https://acme-v02.api.letsencrypt.org/directory` for this module on that node.
|
||||
It is a node property, and the node facing the public internet is the one that opts in.
|
||||
**The module states production.** The binary's staging default stands for anything that runs it
|
||||
without the module — the lab, a developer's machine — and the module's `acme.env` names production
|
||||
for every node it is assigned to, because a node assigned the public proxy is one that serves public
|
||||
traffic.
|
||||
|
||||
| env | default | meaning |
|
||||
|---|---|---|
|
||||
@@ -76,5 +82,5 @@ It is a node property, and the node facing the public internet is the one that o
|
||||
| `LISTEN` | `:80` | HTTP, and the ACME HTTP-01 challenge |
|
||||
| `TLS_LISTEN` | `:443` | HTTPS; unset to serve plain HTTP only |
|
||||
| `ACME_CACHE` | `/acme` | where issued certificates persist; required when `TLS_LISTEN` is set, so a restart does not re-order |
|
||||
| `ACME_DIRECTORY` | Let's Encrypt **staging** | the issuer; a public-serving node overrides it to production |
|
||||
| `ACME_DIRECTORY` | Let's Encrypt **staging** in the binary; production in the module's `acme.env` | the issuer |
|
||||
| `ACME_CA_BUNDLE` | *(unset)* | a file of roots to trust for the issuer's own API — set only for a private/lab authority whose API certificate the world does not yet trust |
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
"route": "${dir:routes-dir}/mesh.json"
|
||||
},
|
||||
"requires": [
|
||||
"acme-ca",
|
||||
"internal-acme-ca"
|
||||
],
|
||||
"binds": {
|
||||
"acme-ca": "${dir:state}/acme-ca.json",
|
||||
"internal-acme-ca": "${dir:state}/internal-acme-ca.json"
|
||||
},
|
||||
"own-secrets": {
|
||||
@@ -80,7 +78,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/acme.env",
|
||||
"mode": "0600",
|
||||
"content": "ACME_DIRECTORY=https://${bound:acme-ca:at}:${bound:acme-ca:port}${bound:acme-ca:path}\nACME_ROOTS=https://${bound:acme-ca:at}:${bound:acme-ca:port}${bound:acme-ca:roots}\nACME_ROOTS_PATH=${bound:acme-ca:roots}\n"
|
||||
"content": "ACME_DIRECTORY=https://acme-v02.api.letsencrypt.org:443/directory\nACME_ROOTS=https://acme-v02.api.letsencrypt.org:443\nACME_ROOTS_PATH=\n"
|
||||
},
|
||||
{
|
||||
"id": "internal-acme-env",
|
||||
|
||||
Reference in New Issue
Block a user