Getting Caddy to issue TLS certificates for Private zones

1. The problem I’m having:

Hi, I am trying to do this.

I have domain.me domain. I want to use *.hs.domain.me domain in my home lab and these services should not be publicly accessible.

For this, I have added a NS record to hs.domain.me that points it to a VPN/internal only DNS server and I have configured all the subdomains there.

Now, I want to use Caddy to put TLS in front of all those private apps. I knew http challenge will not work because these domains will not be accessible from outside and I momentarily forgot DNS01 challenge suffers from the same problem. Caddy adds the secret at _acme-challenge.hs.domain.me but LE/ZeroSSL can not find this secret.

2. Error messages and/or full log output:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

3. Caddy version:

v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=

4. How I installed and ran Caddy:

caddy custom builder

a. System environment:

b. Command:

caddy run --config /etc/caddy/caddy.json

d. My complete Caddy config:

Omitting this for now

5. Links to relevant resources:

Potential Solution

One possible way to make this work is,

  1. A pre-start script which removes the NS record from hs.domain.me. This makes _acme-challenge.hs.domain.me reachable from public.
  2. Start caddy, wait until it has received TLS certificates.
  3. Add the NS record again.

I initially thought of doing this with bash scripts but that’s problematic because caddy will try to renew certificate without restarting and without providing any external signals I can use in my bash script.

What’s a good way to solve this problem ? I guess another option is to fork dns-cloudflare and update it to do this ? It already uses the cloudflare token so I guess it should be as easy as adding two more API calls to add/remove NS records.

Please suggest if there is any other idea I have missed here. Thank you!

1 Like

The caddy.Provisioners API is quite basic. There doesn’t appear to be a way to install hooks before some functions are called in the module.

I was looking at the wrong place!

I found events: Begin implementing event system by mholt · Pull Request #4984 · caddyserver/caddy · GitHub and I’ll try this out

2 Likes

For future reference,

Here is what my config looks like,

{"apps": 
    "events": {
      "subscriptions": [
        {
          "events": ["cert_obtained"],
          "handlers": [
            {
              "handler": "exec",
              "command": "bash",
              "args": [
                "/var/lib/caddy/cert.obtained.sh",
                "{event.data.identifier}"
              ]
            }
          ]
        },
        {
          "events": ["cert_obtaining"],
          "handlers": [
            {
              "handler": "exec",
              "command": "bash",
              "args": [
                "/var/lib/caddy/cert.obtaining.sh",
                "{event.data.identifier}"
              ]
            }
          ]
        }
      ]
    },
    "tls": {
      "automation": {
        "policies": [
          {
            "subjects": [
              "testirc.hs.domain.me",
              "dash.domain.me",
              "git.domain.me",
              "jellyfin.domain.me",
              "ldap.domain.me",
              "qbit.domain.me",
              "mc.domain.me",
              "s.domain.me"
            ],
            "issuers": [
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "",
                      "name": "cloudflare"
                    }
                  }
                },
                "module": "acme"
              },
              {
                "challenges": {
                  "dns": {
                    "provider": {
                      "api_token": "",
                      "name": "cloudflare"
                    }
                  }
                },
                "module": "zerossl"
              }
            ]
          }
        ]
      }
    }
}

And here is what the scripts look like. They can almost definitely be cleaned up a little but it works and I am probably not going to bother with this again. (If I had to, I’ll rewrite it in go then do this again)

# cert-obtained.sh

TOKEN=
PRIVATE_ZONE=hs.domain.me
ZONE=domain.me

set -e
set -o pipefail

echo "Working with domain $1"

if [[ $1 != *.hs.domain.me ]] && [[ $1 != hs.domain.me ]]; then
	exit 0;
fi

ZONE_ID_QUERY=$(echo ".result [] | select(.name == \"$ZONE\") | .id");
echo "Zone ID Query is $ZONE_ID_QUERY"
ZONE_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/zones" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type:application/json" | jq --raw-output "$ZONE_ID_QUERY")
echo $ZONE_ID

record=$(echo "{\"content\": \"ns.home.domain.me\", \"name\": \"hs.domain.me\", \"proxied\": false, \"type\": \"NS\", \"ttl\": 600}")

echo $record

curl -X POST -v "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type:application/json" \
     --data "$record"


# cert-obtaining.sh

TOKEN=
PRIVATE_ZONE=hs.domain.me
ZONE=domain.me

echo "Working with domain $1"

if [[ $1 != *.hs.domain.me ]] && [[ $1 != hs.domain.me ]]; then
	exit 0;
fi

ZONE_ID_QUERY=$(echo ".result [] | select(.name == \"$ZONE\") | .id");
echo "Zone ID Query is $ZONE_ID_QUERY"
ZONE_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/zones" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type:application/json" | jq --raw-output "$ZONE_ID_QUERY")
echo $ZONE_ID


RECORD_ID_QUERY=$(echo ".result [] | select(.name == \"$PRIVATE_ZONE\") | .id");
echo "Zone ID Query is $RECORD_ID_QUERY"
RECORD_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type:application/json" | jq --raw-output "$RECORD_ID_QUERY")
echo $RECORD_ID

curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Content-Type:application/json" || true

sleep 10

Notes:

  1. Here is the list of events and inputs in those events for reference events: Implement event system by francislavoie · Pull Request #4912 · caddyserver/caddy · GitHub
2 Likes

Impressively done! Clever solution :smile:

1 Like

Yes! Wow this is awesome. Thank you for sharing your solution!

1 Like

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