Skip to main content

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:

ModeHow DNS is updatedCert issuance
duckdnsUjex drives DuckDNS API with the owner's tokenAutomatic via issue() — DNS-01 using DuckDNS TXT
customOwner sets DNS manually on their own domainTwo-step via acmeStartacmeFinish 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

CodeReason
invalid-argumenttoken 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

CodeReason
invalid-argumentfqdn or ip missing

Behaviour

  • No-ops when ip equals the last recorded value (changed: false)
  • For .duckdns.org hostnames: 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 to owners/{uid}/hostnames/{fqdn}/ipHistory
  • Audit: gateway.updateIp with {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

CodeReason
invalid-argumentfqdn 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

CodeReason
not-foundHostname not claimed
failed-preconditionMode 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

CodeReason
not-foundorderId doesn't exist
internalLE 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

CodeReason
not-foundHostname not claimed
failed-preconditionNot 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

CodeReason
not-foundNo 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

PathWritten byContents
owners/{uid}/config/duckdnssetDuckDnsTokenciphertext (KMS), kmsKey, updatedAt
owners/{uid}/config/acmeacmeStart/issue (lazy)ACME account key (KMS)
owners/{uid}/hostnames/{fqdn}claimHostname, updateIpmode, currentIp, lastPropagated, timestamps
owners/{uid}/hostnames/{fqdn}/ipHistory/{autoId}updateIpip, propagated, changedAt
owners/{uid}/acmeOrders/{orderId}acmeStart, acmeFinishchallenge + cert key + CSR (KMS), status
owners/{uid}/certificates/{id}acmeFinish, issue, renewCertsfqdn, issuer, notBefore, notAfter, certPemEnc, keyPemEnc, kmsKey

Audit events

ActionActorMeta
duckdns.setTokenhuman
gateway.updateIpeitherip, propagated
gateway.claimHostnameeithermode
gateway.issueeitherstaging, (and mode for custom)