Skip to content

Quickstart

The fastest path: a single host with outbound internet access getting a certificate for a name it can't serve on port 80. This is standalone mode; the other modes build on the same core.

1. Build

$ git clone https://github.com/amitpatole/gapcert && cd gapcert
$ make build
$ ./bin/gapcert version
0.1.0

Go 1.26+ is required to build. The result is a static binary with no runtime dependency.

So gapcert never needs write access to your production DNS zone, pre-create a one-time CNAME pointing the challenge name at a dedicated challenge zone (an acme-dns instance works well):

_acme-challenge.web.internal.example.com.  CNAME  <id>.acme.example.com.

Now a credential that can only write to acme.example.com is enough to prove control of web.internal.example.com. See configuration for the other solvers (Route53, Cloudflare, RFC2136/TSIG).

3. Configure

Write ~/.config/gapcert/config.yaml:

ca: letsencrypt-staging          # switch to `letsencrypt` once it works
email: ops@example.com
certificates:
  - name: web
    domains: ["web.internal.example.com"]
    solver: challenge-zone
    hook: "systemctl reload nginx"   # run after a successful (re)issue
solvers:
  challenge-zone:
    type: acme-dns
    api_base: https://acme-dns.example.com
    storage_path: ~/.config/gapcert/acme-dns.json

Start on staging

Point ca at letsencrypt-staging first. Let's Encrypt's production endpoint has strict rate limits; staging lets you iterate freely. Switch to letsencrypt once a staging issue succeeds.

4. Issue

$ gapcert issue
web                  obtaining [web.internal.example.com] via challenge-zone ...
web                  stored ~/.local/share/gapcert/certs/web
web                  hook ok

The private key was generated locally and written 0600; only a CSR left the host. Check what you have and when it expires:

$ gapcert status
NAME                 DOMAINS                                  EXPIRES        DAYS  STATUS
web                  web.internal.example.com                 2026-10-24       88  ok

5. Keep it renewed

Run gapcert renew from cron or a systemd timer. It re-issues only certificates inside their renewal window, rotates the key, and fires the deploy hook:

0 3 * * *  gapcert renew

That's the whole loop for one host. If your hosts can't reach the internet directly, keep the credentials off every box with the broker; if they have no network at all, use the courier.