Getting SSL certificates via Cloudflare fails if user is not admin

1. The problem I’m having:

I want to run Caddy as a non-administrative user under Windows, as a service, as it is not best practice to run it as an administrator.

When the service runs under a local account that is a member of the Administrators group, caddy works without problems. I take that as confirmation/sanity check that my caddyfile, cloudflare token and overall network configuration are not to blame.

Only when the service runs under a normal user that is not an administrator, caddy is unable to retrieve certificates. It will fail (repeatedly) with the errors below. Therefore, the sites cannot be accessed and browsers will display a certificate error when trying to access them.

As soon as the service user is added to the Administrators group, the directory
C:\Users\proxyuser\AppData\Roaming\Caddy\
purged and the service restarted, caddy will once again work perfectly.

If admin rights are again withdrawn, certificate retrieval will fail again.

What I need to know and was unable to find out:

  • Which permissions do I need to give to the ordinary user for caddy to be able to retrieve the certificates without timeout.

Is it possible that to create the certificate requests, caddy relies on a Windows function or library that is not accessible to normal users, only to administrators?

2. Error messages and/or full log output:

As stated, when running as an admin user, caddy works fine and retrieves the certificates quickly and without any issues.
When running as normal user as service (see section 4 below), caddy is unable to get the certificates (except once for one of the two domains during testing, which I consider spurious) and will usually create these errors:

{"level":"error","ts":1729797523.7216048,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"ab.mydomain.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"[ab.mydomain.com] solving challenges: waiting for solver certmagic.solverWrapper to be ready: timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: <nil> (order=https://acme-v02.api.letsencrypt.org/acme/order/2018563123/316703407123) (ca=https://acme-v02.api.letsencrypt.org/directory)"}
{"level":"error","ts":1729797523.7221572,"logger":"tls.obtain","msg":"will retry","error":"[ab.mydomain.com] Obtain: [ab.mydomain.com] solving challenges: waiting for solver certmagic.solverWrapper to be ready: timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: <nil> (order=https://acme-v02.api.letsencrypt.org/acme/order/2018563234/316703407234) (ca=https://acme-v02.api.letsencrypt.org/directory)","attempt":1,"retrying_in":60,"elapsed":125.5902871,"max_duration":2592000}
{"level":"error","ts":1729797523.7256222,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"cd.mydomain.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"[cd.mydomain.com] solving challenges: waiting for solver certmagic.solverWrapper to be ready: timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: <nil> (order=https://acme-v02.api.letsencrypt.org/acme/order/2018563456/316703407345) (ca=https://acme-v02.api.letsencrypt.org/directory)"}
{"level":"error","ts":1729797523.7256222,"logger":"tls.obtain","msg":"will retry","error":"[cd.mydomain.com] Obtain: [cd.mydomain.com] solving challenges: waiting for solver certmagic.solverWrapper to be ready: timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: <nil> (order=https://acme-v02.api.letsencrypt.org/acme/order/2018563157/316703407567) (ca=https://acme-v02.api.letsencrypt.org/directory)","attempt":1,"retrying_in":60,"elapsed":125.6029214,"max_duration":2592000}

3. Caddy version:

v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

a. System environment:

Clean, fresh Windows Server 2022 install with latest updates as of today. Also tried on a Windows Server 2025 (Core, without desktop experience) machine with identical results.

Windows Firewall has been completely deactivated for troubleshooting purposes, so can be ruled out as culprit. There is no other software installed on the system.

Due to the fact that everything works as soon as local admin rights are given to the caddyservice user account, I believe any external factors (cloudflare config, router, port forwarding etc.) can also be ruled out.

b. Command:

  • Downloaded caddy with cloudflare and replace-response from here.

  • Saved exe and caddyfile to C:\caddy

  • Installed caddy as service with this cmd
    sc.exe create "caddyservice" start=auto binPath="C:\caddy\caddy.exe run"

  • With lusrmgr.msc, created a standard new local Windows user account called “proxyuser” (non-admin, member of default group “Users” only)

  • In services.msc, changed user for caddyservice to “proxyuser” and acknowledged the message to give this account local service permissions.

  • Set service to start automatically.

  • Alternatively, for troubleshooting purposes, caddy was also run as local admin user (member of “Administrators” group) in cmd via
    caddy.exe run --watch
    while the service was stopped.

  • For further troubleshooting, “proxyuser” was intermittently added to the group “Administrators” for testing purposes (while the service was stopped; AppData directory contents deleted before starting service again) which would then solve the errors.

d. My complete Caddy config:

{
	#debug
	order replace after encode

	log default {
		output file C:\\caddy\\log.txt {
			roll_size 1MiB
			roll_local_time
		}
		level WARN
	}
}

https://ab.mydomain.com {
	encode gzip
	reverse_proxy https://10.0.0.10 {
		header_up Accept-Encoding identity
		transport http {
			tls_insecure_skip_verify
		}
	}

	tls {
		dns cloudflare 123mytoken123
		resolvers 1.1.1.1
	}

	replace {
		"Hello" "Bye"
	}
}

https://cd.mydomain.com {
	encode gzip
	root * C:\\caddy\\www\\cd.mydomain.com
	file_server

	tls {
		dns cloudflare 123mytoken123
		resolvers 1.1.1.1
	}
}

:man_shrugging:

We’re not doing anything weird, just using the usual syscalls to open listeners etc. What Windows does is a black box to us, we don’t specialize in Windows development etc.

nil is strange, but this error typically suggests that DNS queries are failing to find the TXT record on your domain (so Caddy doesn’t think issuance completed).

Do non-admins on your machine have DNS queries blocked/limited? You do have resolvers 1.1.1.1 so Caddy should be hitting Cloudflare’s DNS servers directly, but maybe Windows is intercepting those connections and blocking them?

Anyway, you could just turn off propagation checks, it’s an optional “sanity check” that Caddy is doing to make sure it can see the TXT record before telling the ACME issuer “ok now you can look”. Since you’re using Cloudflare, it should be basically instant anyway. So you can add propagation_timeout -1 to turn those off.

2 Likes

It doesn’t sound like you’ve done this from your steps to reproduce, but one thing to check: If the files in %APPDATA% where created with admin rights then the proxyuser might have trouble modifying these when admin rights are withdrawn.

Also I thought non-admin users were restricted from opening ports below 1024 on Windows, which would rule out 80 & 443?

Except that ACME requires Port 80 initiation.

From here RFC 8555 - Automatic Certificate Management Environment (ACME)

“ Given a challenge/response pair, the server verifies the client’s
control of the domain by verifying that the resource was provisioned
as expected.

  1. Construct a URL by populating the URL template [RFC6570]
    “http://{domain}/.well-known/acme-challenge/{token}”, where:

    • the domain field is set to the domain name being verified; and

    • the token field is set to the token in the challenge.

  2. Verify that the resulting URL is well-formed.

  3. Dereference the URL using an HTTP GET request. This request MUST
    be sent to TCP port 80 on the HTTP server.

  4. Verify that the body of the response is a well-formed key
    authorization. The server SHOULD ignore whitespace characters at
    the end of the body.

  5. Verify that key authorization provided by the HTTP server matches
    the key authorization stored by the server.”

And thus admin privileges are required.

Edit

And Let’s Encrypt specifically
The HTTP-01 challenge of the Challenge Types - Let's Encrypt states
“The HTTP-01 challenge can only be done on port 80.”
“Our implementation of the HTTP-01 challenge follows redirects, up to 10 redirects deep. It only accepts redirects to “http:” or “https:”, and only to ports 80 or 443. It does not accept redirects to IP addresses. When redirected to an HTTPS URL, it does not validate certificates (since this challenge is intended to bootstrap valid certificates, it may encounter self-signed or expired certificates along the way).”

Best Practice - Keep Port 80 Open

Edit 2

From CA/Browser Forum Baseline Requirement .

Authorized Ports: One of the following ports: 80 (http), 443 (https), 25 (smtp), 22 (ssh).

3.2.2.4.18 Agreed‐Upon Change to Website v2
Confirming the Applicant’s control over the FQDN by verifying that the Request Token or Random Value is contained in the contents of a file.

  1. The entire Request Token or Random Value MUST NOT appear in the request used to retrieve the file, and
  2. the CA MUST receive a successful HTTP response from the request (meaning a 2xx HTTP status code must be received).
    The file containing the Request Token or Random Value:
  3. MUST be located on the Authorization Domain Name, and
  4. MUST be located under the “/.well‐known/pki‐validation” directory, and 3. MUST be retrieved via either the “http” or “https” scheme, and
  5. MUST be accessed over an Authorized Port

I’m guessing he’s using DNS-01 challenges since he’s using a cloudflare token.
Port 80 inbound isn’t required for that, but 80 & 443 are surely required for the use of the actual reverse proxy functionality unless it’s a non-standard port.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.