Setup TLS & certificates

Certificates, and the errors that come from them

Conduck accepts two kinds of address, and no others. An https:// address whose certificate this device already trusts by itself. Or a plain http:// address that only your own network can reach. Everything else is refused before a request is sent.

This is Apple's rule, not a setting: App Transport Security lets an app make certificate checks stricter and refuses to let it make them looser, so an untrusted certificate fails below the app, where nothing in Conduck can reach it. The practical answer is that a trusted certificate is free — three routes below hand you one, and the fastest is a single command.

What the error means

This device doesn't trust the certificate

One refusal covers four different server-side situations, because from the device they are the same thing: nothing the device already trusts vouches for the certificate it was shown.

  • A certificate the server signed itself.
  • A certificate from a private authority — a company CA, a home-lab CA, or Caddy's own local CA, which is the one that catches people out because it looks like a real setup.
  • A certificate that has expired.
  • A chain missing its intermediate certificate. The certificate itself is fine and a browser you tested with may have filled the gap from cache — the device can't, so it can't connect the certificate back to an authority it knows. Serve the full chain (fullchain.pem, not cert.pem).

The fix is on the server in all four cases, and it is free in all four. Pick a route:

A certificate you signed yourself

A self-signed certificate is never trusted, however it is presented, because nothing has vouched for it except itself. That verdict is reached inside the platform, so it is the same on iPhone, iPad, Mac, and Watch, and it is the same for every app on the device. Conduck cannot make an exception for one, and neither can conduck-connect. Take one of the three routes above instead — each of them ends with a certificate every device already trusts, and none of them costs anything.

A certificate that expired

This one just needs renewing. Two things to check first, because both look like an expiry and neither is one: the clock on the device (a wrong date reads a perfectly good certificate as expired), and whether the server actually reloaded after the renewal — a renewed file on disk that the running process never re-read still serves the old certificate. If a route below renews for you, it also reloads; that is most of why they are worth using.

The certificate doesn't match the fingerprint you pinned

Certificate pinning is optional and off unless you typed a fingerprint into that connection's settings yourself. When it is on, this error means something specific and narrow: the device did trust the certificate chain, and the key underneath it still disagreed with the fingerprint you saved.

There are two readings, and only you can tell them apart.

  • You replaced the certificate. Update the pinned fingerprint in that connection's settings. Worth knowing: most automated renewals mint a fresh key each time, so a route that renews for you will trip a pin on its own schedule. That is the main reason not pinning is the recommended setup — with no fingerprint saved, ordinary system validation applies, which is exactly what the free routes below are built for.
  • You changed nothing. Then stop. System trust already succeeded, so the only remaining explanation for a different key is that something between this device and your server is terminating the connection. Check the network you are on before you touch anything in the app.

Your pin can't be checked at all

Nothing disagreed with anything here, and your server is fine. The chain is trusted; Conduck simply cannot compute a fingerprint for that certificate's key type, so the comparison never ran. The key types it can't hash are Ed25519, RSA-1024, RSA-8192, and EC P-521.

Two ways forward, and the first keeps your pin:

  • Reissue the certificate with an RSA 2048, 3072, or 4096 key, or an EC P-256 or P-384 key.
  • Clear the saved fingerprint in that connection's settings and go back to ordinary system trust. That is safe here and nowhere else on this page: system trust is already passing, and clearing the pin returns the connection to the evaluation that is passing.

A plain http:// address was refused

Plain http:// is accepted only toward an address that nothing outside your own network can reach — and what decides that is the address you typed, not the network the machine happens to sit on. The platform reads the string and answers before any request goes out, which is why Conduck tells you the moment you enter it rather than failing later.

Accepted on plain http://

  • A private IP literal: 192.168.1.20, 10.0.0.5, 172.16.0.4
  • A loopback address: 127.0.0.1, [::1]
  • Link-local: 169.254.x.x, [fe80::…]
  • IPv6 unique-local: [fc00::…], [fd00::…]
  • A Bonjour name ending in .local: mac-mini.local

Refused on plain http://

  • Any dotted domain name, however private the machine behind it — nas.home.arpa, box.lan, ai.example.com
  • A bare single-label name like nas or ollama — a resolver can carry one of those to the public DNS root, and an unencrypted request takes your key with it
  • The 100.64100.127 range an overlay VPN hands out, and .ts.net names. A Tailscale address still needs https://

So: enter the machine's address on your network as a literal, or its .local name, or put HTTPS in front of it. Two traps worth naming — localhost typed on a phone means the phone itself, and most local servers listen on 127.0.0.1 alone, so use the machine's network address instead; and on iOS, reaching anything on your home network also needs Conduck's Local Network permission, in the iOS Settings app.

Staying on plain http at home is a legitimate choice with a real cost — what it buys and what it gives up. To use the server from anywhere, take one of the certificate routes instead.

Your server couldn't be reached

Read the uncertainty in this one first, because it is the part that costs money: Conduck can't tell whether the request arrived. The connection dropped, or never returned a verdict. If your agent runs tools, check the server before sending the same thing again — it may already be doing it.

Then work down the address, which is where this usually lives:

  • Is the address still the right one? A Cloudflare quick tunnel — the *.trycloudflare.com address you get without a domain — is handed a new hostname every restart, so a setup that worked last night points at nothing this morning.
  • Does it answer from somewhere else? Open <your address>/v1/models in a browser on a different device. If that fails too, the problem is not in Conduck.
  • Is the server listening beyond loopback? A process bound to 127.0.0.1 answers on its own machine and nowhere else.
  • iCloud Private Relay. It can take over name lookups while Tailscale still reports connected, sending a .ts.net name to the public internet and getting back an address that answers on none of the ports Conduck uses. Turn it off in Settings → your name → iCloud → Private Relay and test again.
The full troubleshooting ladder

Your server took too long to answer

This is not the same as unreachable, and the difference is the thing to act on: something answered, and it may still be working on your message right now. Another attempt can repeat the whole turn — the tool run, the tokens, the cost on your own provider key. Check the server before you retry.

The usual causes, in the order they turn up:

  • A large model on a small machine, still generating.
  • A tool run that is genuinely long — a build, a search, a file the agent is still writing.
  • A proxy in front with a shorter read timeout than the model needs. Nginx defaults to 60 seconds, which most agent turns exceed; raise proxy_read_timeout on that route rather than shortening the work.

The request was refused (401 or 403)

Two unrelated faults end in the same refusal. Start with the one that has nothing to do with credentials, because it is the one people never suspect: the request was rejected as it arrived, before any token was looked at. On a server you set up as keyless there is no token to reject at all, so the address the request was addressed to is the whole story — see the Host-header trap, which is the common shape of this.

Then the credential. If your server does take a token, copy it again from the server and re-pair. If the token is definitely right, check what sits in front of it: a reverse proxy or an access policy such as Cloudflare Access can reject the request before your server ever sees it. The tell for that one is a login page in a browser where you expected JSON.

Three free routes to a trusted certificate

Tailscale Serve

The shortest route, and the one to pick unless you specifically need a standalone Apple Watch. Tailscale issues a trusted certificate for your machine and keeps the address reachable only from your own devices — nothing is published to the internet and no ports are opened. Run this on the machine your server is on, with your server's local port in place of 8080:

on your server's machine
tailscale serve localhost:8080

Enable MagicDNS and then HTTPS certificates in the Tailscale admin console first, in that order — Serve has nothing to issue a certificate for otherwise. What you get back is an address shaped like https://<machine>.<tailnet>.ts.net. Give that to Conduck as the base address, with no /v1 and no other path on the end.

  • Every device you use Conduck on has to be signed in to the same tailnet. An Apple Watch rides along on a nearby iPhone; a standalone Watch cannot, because there is no Tailscale app on watchOS.
  • Undo it with tailscale serve localhost:8080 off, or clear everything with tailscale serve reset.
  • The public variant is tailscale funnel localhost:8080 — Tailscale calls it Funnel, and it puts the address on the open internet so anyone who finds it can knock. Your server's key is then the only lock, which is why conduck-connect refuses to publish a keyless server this way. Choose it only for a standalone Watch or a device that cannot run Tailscale.

Tailscale's own documentation is the current authority on the command and the console settings: Tailscale Serve.

A domain in front, with Caddy and Let's Encrypt

If you own a domain name and the machine is reachable from the internet, a reverse proxy gets a Let's Encrypt certificate and renews it on its own. Caddy is the least-configuration version of this — a whole Caddyfile for the job is three lines:

Caddyfile
ai.example.com {
    reverse_proxy 127.0.0.1:8080
}

It needs two things to be true before it can issue anything: public DNS for that name has to point at this machine, and ports 80 and 443 have to be reachable from the internet at that address. Both are worth checking from outside your own network, not from the machine itself.

The trap: Caddy's own local CA does not count

When Caddy cannot complete the challenge — and when you write tls internal on purpose — it issues from its own local authority instead. The site then loads perfectly in a browser on that machine, because Caddy installed its root there, while every phone, Mac, and Watch refuses it. The symptom is an untrusted certificate on what looks like a working HTTPS setup. Check the certificate's issuer from another device, not the padlock on that one.

One more thing the proxy has to get right: whatever sits in front must pass requests through unchanged, so that <your address>/v1/models answers with your server's own list of models. If that one address works in a browser, the rest of this will work. And if your server checks which hostname a request was addressed to, the proxy has to rewrite that line — see the Host-header trap.

A certificate for a bare IP address

No domain name at all? Let's Encrypt has issued certificates for bare IP addresses since January 2026, so a rented server with a public IP can have a trusted certificate without anybody buying a name. Two limits decide whether this route is open to you:

  • Public IP only. A private 192.168.x or 10.x address cannot be validated this way, and neither can a home connection sitting behind carrier-grade NAT. If that is your situation, use Tailscale Serve instead — it is the better answer for a machine at home anyway.
  • These certificates are short-lived by policy. IP certificates are issued only on Let's Encrypt's shortlived profile, which is about six days — roughly 160 hours. Automated renewal is not optional here; a renewal you do by hand will lapse mid-week, and the app's verdict when it does is an expired certificate.

Validation for an IP is http-01 or tls-alpn-01, so port 80 or 443 has to be reachable from the internet at that address; the DNS challenge does not apply, because there is no name to prove control of. Certbot 5.4 and later takes both flags:

on the server, with your own IP
certbot certonly --standalone --ip-address -d 203.0.113.10 --preferred-profile shortlived

Certbot obtains the certificate; it does not wire it into whatever serves your traffic. Point your server or proxy at the issued fullchain.pem and privkey.pem, and make sure the renewal hook reloads that process — on a six-day certificate, a reload nobody arranged is next week's outage. Caddy's automatic HTTPS does not issue for IP addresses yet, so this route is an ACME client plus your own configuration rather than the one-line Caddyfile above.

Let's Encrypt's own pages are the current authority on the profile and its limits: 6-day and IP address certificates.

Situations that need a different answer

Staying on plain http at home

This is a real option, not a fallback for people who gave up. A great many self-hosted servers serve plain HTTP on the local network and cannot be configured to do anything else — Ollama on :11434, LM Studio, an Open WebUI box in the next room — and if you only ever use yours at home, there is nothing to arrange. Enter it as a private IP literal (http://192.168.1.20:11434) or a .local name and it saves.

What it gives up, stated plainly, because being told is the whole of the consent:

  • The traffic is not encrypted. Anyone else on that network can read your messages and the key that goes with them.
  • It works only while you are on that network. Not in the car, not out with the Watch, not on cellular.
  • Conduck says so wherever you review that connection. That warning does not switch off, and there is no setting that binds the address to a network to quiet it.

One consequence people meet by surprise: a certificate pin on a plain-http address is refused rather than quietly ignored. Nothing hands over a certificate on that address, so accepting the pair would leave you believing a protection is running that is not.

The Host-header trap

Some servers look at which hostname a request was addressed to and refuse anything they do not recognise. Ollama is the common one: it serves requests addressed to a local name or address and refuses the rest, while a tunnel or proxy usually forwards your public hostname through unchanged. The tell is exact — the server answers perfectly on its own machine and refuses the identical request through your HTTPS address.

Fix it at the front, by rewriting that line before it reaches the server. In nginx that is one directive on the route:

proxy_set_header Host 127.0.0.1:11434;

If your tunnel cannot rewrite headers itself, point it at a small local proxy that can rather than at Ollama directly. Setting OLLAMA_HOST=0.0.0.0 also clears the check, at the cost of answering on every interface on that machine. OLLAMA_ORIGINS does not help at all: it allow-lists browser origins for CORS, and Conduck sends none.

Apple Watch on its own

With your iPhone nearby, the Watch reaches your server through the phone, so a Tailscale-only server works and nothing extra is needed. A standalone Watch — away from the phone, on its own Wi-Fi or cellular — cannot join a tailnet, because there is no Tailscale app on watchOS.

That Watch needs an address it can reach directly: Tailscale Funnel, a Cloudflare Tunnel, or your own HTTPS front. All three make the address public, and that trade is the whole decision — a public address means your server's key is the only thing standing in front of it, so a keyless server should not take this route at all. If a standalone Watch is not something you actually use, stay private and skip it.

A network with no route to the internet

None of the three free routes applies here: each of them proves control of a name or an address to an authority on the internet, and an isolated network cannot complete that exchange. Your own private CA does work — and there is no low-friction bypass on the client side. Every device that will connect has to be explicitly configured to trust that CA, which on iPhone, iPad, and Mac means an installed and enabled certificate profile. That is device-management work, done per device, and nothing in Conduck shortens it.

What still works with no certificate at all is the plain-HTTP route, as long as the server's address is a private literal or a .local name — which on an isolated network it usually already is.

Let the setup tool do it

conduck-connect walks the HTTPS step for you: it detects what you are running, offers Tailscale or Cloudflare, asks before every change, verifies the address with real requests, and prints a setup code once it all passes.

on your server's machine
curl -fsSLO https://github.com/gigaduckai/conduck-connect/releases/latest/download/conduck-connect.sh && bash conduck-connect.sh

Read conduck-connect on GitHub

Full setup guide