Gateway — Cloud Function reference
DuckDNS-style DNS claim + Let's Encrypt ACME certificate issuance for local servers. See the Gateway subsystem for the narrative.
Two hostname modes:
| Mode | How DNS is updated | Cert issuance |
|---|---|---|
duckdns | Ujex drives DuckDNS API with the owner's token | Automatic via issue() — DNS-01 using DuckDNS TXT |
custom | Owner sets DNS manually on their own domain | Two-step via acmeStart → acmeFinish with a DNS-01 TXT record the owner publishes |
All plaintext secrets (DuckDNS tokens, ACME account keys, cert+key PEMs)
are KMS-encrypted at rest with
projects/axy-ujex/locations/us-central1/keyRings/ujex/cryptoKeys/secrets.
setDuckDnsToken
Caller: authenticated human onCall
setDuckDnsToken({token: string}) => {ok: true}
Errors
| Code | Reason |
|---|---|
invalid-argument | token missing |
Writes the KMS-encrypted token to owners/{uid}/config/duckdns. Required
before issue() for any *.duckdns.org hostname.
updateIp
Caller: authenticated (human or agent) onCall
updateIp({fqdn: string; ip: string}) => {
changed: boolean;
propagated: boolean | null; // null = not a duckdns fqdn; bool = DuckDNS API return
}
Errors
| Code | Reason |
|---|---|
invalid-argument | fqdn or ip missing |
Behaviour
- No-ops when
ipequals the last recorded value (changed: false) - For
.duckdns.orghostnames: hits the DuckDNS update API with the owner's token and returns propagation status - For other hostnames: just updates the Ujex record; the owner is responsible for DNS elsewhere
- Writes
owners/{uid}/hostnames/{fqdn}and an entry toowners/{uid}/hostnames/{fqdn}/ipHistory - Audit:
gateway.updateIpwith{ip, propagated}
claimHostname
Caller: authenticated (human or agent) onCall
claimHostname({
fqdn: string;
mode?: 'duckdns' | 'custom'; // defaults based on suffix
}) => {ok: true; mode: 'duckdns' | 'custom'}
Errors
| Code | Reason |
|---|---|
invalid-argument | fqdn is malformed |
Creates owners/{uid}/hostnames/{fqdn} with the intended mode. Required
before any issue() / acmeStart() call. Idempotent.
acmeStart
Caller: authenticated (human or agent) onCall, 60s timeout
acmeStart({
fqdn: string;
staging?: boolean; // use LE staging directory (for testing)
}) => {
challenge: 'dns-01';
recordName: string; // owner must publish: _acme-challenge.<fqdn>
recordValue: string; // TXT value
orderId: string; // pass to acmeFinish after DNS propagates
}
Errors
| Code | Reason |
|---|---|
not-found | Hostname not claimed |
failed-precondition | Mode is duckdns — use issue() instead; or LE did not offer DNS-01 |
Begins an ACME order for a custom-mode hostname, stores the
challenge + CSR in owners/{uid}/acmeOrders/{orderId}. The owner must
publish the TXT record before calling acmeFinish.
acmeFinish
Caller: authenticated (human or agent) onCall, 540s timeout, 512 MiB
acmeFinish({orderId: string}) => {
issued: true;
id: string; // certificate doc id, pass to getCert
notAfter: string; // ISO-8601 90 days out
}
Errors
| Code | Reason |
|---|---|
not-found | orderId doesn't exist |
internal | LE refused the challenge (TXT not propagated yet) |
Completes the ACME flow after the owner publishes the TXT record.
Writes the cert + key (KMS-encrypted) to
owners/{uid}/certificates/{id}. Audit: gateway.issue.
issue
Caller: authenticated (human or agent) onCall, 540s timeout, 512 MiB
issue({
fqdn: string;
staging?: boolean;
}) => {
issued: true;
id: string;
notAfter: string;
}
Errors
| Code | Reason |
|---|---|
not-found | Hostname not claimed |
failed-precondition | Not a .duckdns.org hostname, or setDuckDnsToken hasn't been called |
One-shot ACME for *.duckdns.org — Ujex drives both the DNS-01
challenge (by setting the TXT on DuckDNS) and the finalise step. Stores
the cert in owners/{uid}/certificates/{id}. Audit: gateway.issue
with {staging}.
getCert
Caller: authenticated (human or agent) onCall
getCert({fqdn: string; id?: string}) => {
certPem: string;
keyPem: string;
notAfter: string;
}
Errors
| Code | Reason |
|---|---|
not-found | No certificate matches |
Returns the latest cert for fqdn (or the specific one identified by
id). Plaintext — sourced from KMS-decrypted PEMs stored in Firestore.
renewCerts
Caller: Cloud Scheduler (internal, every 24h, 540s timeout, 512 MiB) onSchedule — not callable by users
Scans for certificates whose notAfter is within 30 days. For
.duckdns.org certs (non-stub issuer), re-runs the DuckDNS ACME flow
and swaps in the new PEMs + notAfter. Up to 20 certs per tick.
Firestore state
| Path | Written by | Contents |
|---|---|---|
owners/{uid}/config/duckdns | setDuckDnsToken | ciphertext (KMS), kmsKey, updatedAt |
owners/{uid}/config/acme | acmeStart/issue (lazy) | ACME account key (KMS) |
owners/{uid}/hostnames/{fqdn} | claimHostname, updateIp | mode, currentIp, lastPropagated, timestamps |
owners/{uid}/hostnames/{fqdn}/ipHistory/{autoId} | updateIp | ip, propagated, changedAt |
owners/{uid}/acmeOrders/{orderId} | acmeStart, acmeFinish | challenge + cert key + CSR (KMS), status |
owners/{uid}/certificates/{id} | acmeFinish, issue, renewCerts | fqdn, issuer, notBefore, notAfter, certPemEnc, keyPemEnc, kmsKey |
Audit events
| Action | Actor | Meta |
|---|---|---|
duckdns.setToken | human | — |
gateway.updateIp | either | ip, propagated |
gateway.claimHostname | either | mode |
gateway.issue | either | staging, (and mode for custom) |