Quickstart: automatic HTTPS with an Ingress
Take a kind: Service already running on one node, put a real domain in front of it, and get a browser-trusted certificate that issues and renews itself, with no manual intervention, ever.
Prerequisites
- Everything the static website quickstart needs: a FreeBSD 15 host with jails, ZFS, and VNET usable, root access, and Keel built from source.
- A real domain name, with its DNS hosted at OVH (the only DNS provider this milestone automates). If your domain lives elsewhere, this feature isn't usable yet without writing a new
DnsProviderimplementation; see Architecture. - An OVH API application key, application secret, and consumer key, scoped to
GET/POST/PUT/DELETEon/domain/zone/<your-zone>/*. Create these at OVH's API token page; the consumer key needs a follow-up browser approval before it's active. - A single node running both
keel-controlplaneandkeel-agentdregistered to it: anIngress's backend must be akind: Service, and onlykeel-controlplaneunderstands services, even in a cluster of one. - The node's public IP pointed at your domain (an A record), and
pfavailable to redirect inbound traffic to the ingress jail.
1. Prepare two base images
The ingress jail runs nginx; your backend runs whatever you're actually serving. This walkthrough reuses the static site from the first quickstart as the backend, so you need one additional base image with nginx installed, cloned exactly the way every base image is:
# as root zfs create -p zroot/keel/base/keel-ingress tar -xf base.txz -C /zroot/keel/base/keel-ingress cp /etc/resolv.conf /zroot/keel/base/keel-ingress/etc/resolv.conf pkg -c /zroot/keel/base/keel-ingress install -y nginx # nginx's package may default its config path differently depending on # your FreeBSD release and package set; confirm which one before moving # on, since keel-agentd always writes to /usr/local/etc/nginx/nginx.conf # and passes -c explicitly, regardless of nginx's own compiled-in default. chroot /zroot/keel/base/keel-ingress /usr/local/sbin/nginx -c /usr/local/etc/nginx/nginx.conf -t
Also make sure the base image's dynamic linker actually knows where nginx's shared libraries live: a freshly extracted base system plus a pkg -c install doesn't always populate this on its own:
chroot /zroot/keel/base/keel-ingress /sbin/ldconfig -m /usr/local/lib
2. Configure OVH credentials and start the daemons
Write the OVH credentials to a daemon config file: never into a spec, since these are secrets, not per-Ingress data:
# /usr/local/etc/keel/dns-ovh.toml app_key = "your-application-key" app_secret = "your-application-secret" consumer_key = "your-consumer-key" zone = "example.com"
Start keel-controlplane, then keel-agentd registered to it, pointing the ACME client at Let's Encrypt's staging directory first: staging has none of production's rate limits, so this is where you iterate:
# as root, one terminal keel-controlplane --addr 127.0.0.1:7620 \ --cluster-cidr 10.0.0.0/16 --service-cidr 10.0.250.0/24 \ --tls-ca-file /etc/keel/ca.crt --tls-cert-file /etc/keel/controlplane.crt \ --tls-key-file /etc/keel/controlplane.key --tls-crl-file /etc/keel/crl.pem # as root, another terminal keel-agentd --pool zroot --node-id node-1 \ --control-plane-addr 127.0.0.1:7620 \ --advertise-addr 127.0.0.1:7621 --replicate-addr 127.0.0.1:7622 \ --tls-ca-file /etc/keel/ca.crt --tls-cert-file /etc/keel/node-1.crt \ --tls-key-file /etc/keel/node-1.key --tls-crl-file /etc/keel/crl.pem \ --dns-ovh-config /usr/local/etc/keel/dns-ovh.toml \ --acme-directory-url https://acme-staging-v02.api.letsencrypt.org/directory \ --acme-account-key-file /var/db/keel/acme-account.json \ --public-iface vtnet0
See the CLI reference for generating the mutual-TLS certificates these flags reference with scripts/gen-certs.sh. --public-iface is the node's real public network interface; keel-agentd uses it to write the pf redirect rules in step 5.
3. Turn your jail into a Service
An Ingress's backend must be a kind: Service, never a bare kind: Jail, so it always has a stable virtual IP to proxy to, even a single-replica one. Reusing the static site from the first quickstart:
apiVersion: keel/v1
kind: Service
metadata:
name: hugo-site
spec:
replicas: 1
port: 8080
template:
image: base/14.2-web
command: ["/usr/local/bin/python3", "-m", "http.server", "8080", "--directory", "/srv/www"]
network:
vnet: true
bridge: keel0
resources:
cpu: "1"
memory: "256M"
restartPolicy: Always
keelctl --control-plane-addr 127.0.0.1:7620 \ --tls-ca-file /etc/keel/ca.crt --tls-cert-file /etc/keel/admin.crt \ --tls-key-file /etc/keel/admin.key --tls-crl-file /etc/keel/crl.pem \ apply -f service.yaml
4. Apply the Ingress
Save the following as ingress.yaml, using your own real domain and a real contact email: Let's Encrypt requires both to be genuine, and spec.host becomes the certificate's subject:
apiVersion: keel/v1
kind: Ingress
metadata:
name: blog
spec:
host: example.com
backend:
service: hugo-site
port: 8080
tls:
email: admin@example.com
Apply it directly to the node's own socket, not through the control plane: an Ingress is never scheduled, it always applies to whichever node it's given to.
keelctl apply -f ingress.yaml
On its next 5-second reconcile tick, keel-agentd provisions the singleton keel-ingress jail (once, shared by every Ingress you apply from here on), places a real ACME order, publishes the DNS-01 challenge as a TXT record via the real OVH API, waits for it to propagate across public DNS, and downloads a real (staging) certificate once Let's Encrypt validates it. Watch it happen:
keelctl get blog jls # a new keel-ingress jail should appear alongside your backend's
5. Go public
Two host-level things need to be true for a real browser, anywhere on the internet, to actually reach the ingress jail: pf needs to forward the public IP's ports 80/443 to it, and the kernel needs to be willing to route between the public interface and the internal bridge at all.
# as root, once sysctl net.inet.ip.forwarding=1 echo 'gateway_enable="YES"' >> /etc/rc.conf # persists across reboots
keel-agentd (started with --public-iface above) writes its own redirect rules into a named pf anchor on its own schedule, but your host's own top-level pf.conf needs to declare that anchor once, since Keel intentionally never touches a file it doesn't fully own:
# in /etc/pf.conf, near the top (translation rules) and bottom (filter rules) rdr-anchor "keel-ingress" # ... your own existing rules ... anchor "keel-ingress"
Reload pf after editing it (pfctl -f /etc/pf.conf), and make sure your firewall also has an explicit pass rule for the internal jail bridge itself (keel0 by convention): a fresh host's default-deny policy blocks jail-to-service traffic just as readily as it blocks the internet, and without it nginx's own proxy_pass to your backend's virtual IP times out even once the public redirect itself is working.
6. Visit the site
curl -sk https://example.com/ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \ | openssl x509 -noout -issuer
The issuer line should mention (STAGING), confirming you're looking at a real, working staging certificate. Once you're satisfied everything works, switch to production by stopping keel-agentd, changing --acme-directory-url to https://acme-v02.api.letsencrypt.org/directory, pointing --acme-account-key-file at a fresh path (a staging account can't be reused against production; ACME accounts are directory-specific), and restarting. Expect the certificate's issuer to read plain Let's Encrypt afterward, no (STAGING), and curl to succeed with no -k at all, since it's now signed by a publicly trusted CA.
7. Renewal needs nothing from you
A certificate within 30 days of expiring is renewed automatically on the node's next reconcile tick, the same DNS-01 flow as first issuance, with no manual step and no downtime: the previous certificate keeps serving until the new one is ready. This is the entire point of the feature: once steps 1 through 6 are done, there is nothing left to remember to do again.
Clean up
keelctl delete blog # removes this Ingress's server block; the shared nginx jail stays up keelctl --control-plane-addr 127.0.0.1:7620 \ --tls-ca-file /etc/keel/ca.crt --tls-cert-file /etc/keel/admin.crt \ --tls-key-file /etc/keel/admin.key --tls-crl-file /etc/keel/crl.pem \ delete hugo-site
Next steps
This walkthrough covers a single node with one public IP, which is the only topology this milestone supports: an Ingress and every replica of its backend Service must live on the same node, and there's no wildcard, multi-domain, or path-based routing yet. See the Architecture page and the JailSpec reference for exactly what's built today and what's deliberately deferred.
Keel