ACME Gandi.net renewal
-
Gandi moved away from using API Keys (just a key) and is now using PAT tokens (Name + Key). Regarding DynDNS pfSense is already supporting PAT with the latest BETA release. However the ACME service still uses the old deprecated API Key. Any ideas for a workaround?
-
@ITSGS_ said in ACME Gandi.net renewal:
However the ACME service still uses the old deprecated API Key
You've checked with the original (aka : the source) ?
If this is current version : https://github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gandi_livedns - and it is since .. not sure, 8 months now. The acme.sh version of pfSense was synced with the source more recent then that.
Then this file, the Gandi Live DNS implementation should match what you have : see here, on your pfSense : /usr/local/pkg/acme/dnsapi/dns_gandi_livedns.shI don't use Gandi myself, so I can't test.
Not related, just a question : the certificate is used for a VPN server ?
-
@Gertjan Thank you for the directions. I will have a look this evening and post some logs. I tried it using my current PAT Token for my domain. The cert is for a website, not for a VPN-Server. :-)
-
@Gertjan: I had a look at the files and logs. The Personal Access Token (PAT) is indeed implementet in the script. However I do not know how to choose PAT. I do not understand the place how the code decides, if it's a PAT or the old deprecated API-Key.
EDIT: I created a new PAT to make sure that I had no error copying. The issue remains the same.
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then export _H2="Authorization: Bearer $GANDI_LIVEDNS_TOKEN" else export _H2="Authorization: Apikey $GANDI_LIVEDNS_KEY" fi
#Usage: dns_gandi_livedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
The log is pretty clear:
[Sat Jun 28 10:35:03 CEST 2025] 5:GANDI_LIVEDNS_KEY='HEREISMYPATTOKEN(NOTOLDAPIKEY)' [Sat Jun 28 10:35:03 CEST 2025] First detect the root zone [Sat Jun 28 10:35:03 CEST 2025] h='vpn.mydomain.cloud' [Sat Jun 28 10:35:03 CEST 2025] domains/vpn.mydomain.cloud [Sat Jun 28 10:35:03 CEST 2025] GET [Sat Jun 28 10:35:03 CEST 2025] url='https://api.gandi.net/v5/livedns/domains/vpn.mydomain.cloud' [Sat Jun 28 10:35:03 CEST 2025] timeout= [Sat Jun 28 10:35:03 CEST 2025] Http already initialized. [Sat Jun 28 10:35:03 CEST 2025] _CURL='curl --silent --dump-header /tmp/acme/vpn.mydomain.cloud/http.header -L -g ' [Sat Jun 28 10:35:03 CEST 2025] ret='0' [Sat Jun 28 10:35:03 CEST 2025] response='{"object":"HTTPForbidden","cause":"Forbidden","code":403,"message":"Access was denied to this resource."}'
/usr/local/pkg/acme/dnsapi/dns_gandi_livedns.sh:
dns_gandi_livedns_info='Gandi.net LiveDNS Site: Gandi.net/domain/dns Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gandi_livedns Options: GANDI_LIVEDNS_KEY API Key Issues: github.com/fcrozat/acme.sh Author: Frédéric Crozat <fcrozat@suse.com>, Dominik Röttsches <drott@google.com> ' # Gandi LiveDNS v5 API # https://api.gandi.net/docs/livedns/ # https://api.gandi.net/docs/authentication/ for token + apikey (deprecated) authentication # currently under beta ######## Public functions ##################### GANDI_LIVEDNS_API="https://api.gandi.net/v5/livedns" #Usage: dns_gandi_livedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_gandi_livedns_add() { fulldomain=$1 txtvalue=$2 if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then _err "No Token or API key (deprecated) specified for Gandi LiveDNS." _err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively" return 1 fi # Keep only one secret in configuration if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then _saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN" _clearaccountconf GANDI_LIVEDNS_KEY elif [ -n "$GANDI_LIVEDNS_KEY" ]; then _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" _clearaccountconf GANDI_LIVEDNS_TOKEN fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" return 1 fi _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" _debug domain "$_domain" _debug sub_domain "$_sub_domain" _dns_gandi_append_record "$_domain" "$_sub_domain" "$txtvalue" } #Usage: fulldomain txtvalue #Remove the txt record after validation. dns_gandi_livedns_rm() { fulldomain=$1 txtvalue=$2 _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" return 1 fi _debug fulldomain "$fulldomain" _debug domain "$_domain" _debug sub_domain "$_sub_domain" _debug txtvalue "$txtvalue" if ! _dns_gandi_existing_rrset_values "$_domain" "$_sub_domain"; then return 1 fi _new_rrset_values=$(echo "$_rrset_values" | sed "s/...$txtvalue...//g") # Cleanup dangling commata. _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, ,/ ,/g") _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, *\]/\]/g") _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g") _debug "New rrset_values" "$_new_rrset_values" _gandi_livedns_rest PUT \ "domains/$_domain/records/$_sub_domain/TXT" \ "{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" && _contains "$response" '{"message":"DNS Record Created"}' && _info "Removing record $(__green "success")" } #################### Private functions below ################################## #_acme-challenge.www.domain.com #returns # _sub_domain=_acme-challenge.www # _domain=domain.com _get_root() { domain=$1 i=2 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) _debug h "$h" if [ -z "$h" ]; then #not valid return 1 fi if ! _gandi_livedns_rest GET "domains/$h"; then return 1 fi if _contains "$response" '"code": 401'; then _err "$response" return 1 elif _contains "$response" '"code": 404'; then _debug "$h not found" else _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" return 0 fi p="$i" i=$(_math "$i" + 1) done return 1 } _dns_gandi_append_record() { domain=$1 sub_domain=$2 txtvalue=$3 if _dns_gandi_existing_rrset_values "$domain" "$sub_domain"; then _debug "Appending new value" _rrset_values=$(echo "$_rrset_values" | sed "s/\"]/\",\"$txtvalue\"]/") else _debug "Creating new record" "$_rrset_values" _rrset_values="[\"$txtvalue\"]" fi _debug new_rrset_values "$_rrset_values" _gandi_livedns_rest PUT "domains/$_domain/records/$sub_domain/TXT" \ "{\"rrset_ttl\": 300, \"rrset_values\": $_rrset_values}" && _contains "$response" '{"message":"DNS Record Created"}' && _info "Adding record $(__green "success")" } _dns_gandi_existing_rrset_values() { domain=$1 sub_domain=$2 if ! _gandi_livedns_rest GET "domains/$domain/records/$sub_domain"; then return 1 fi if ! _contains "$response" '"rrset_type":"TXT"'; then _debug "Does not have a _acme-challenge TXT record yet." return 1 fi if _contains "$response" '"rrset_values":\[\]'; then _debug "Empty rrset_values for TXT record, no previous TXT record." return 1 fi _debug "Already has TXT record." _rrset_values=$(echo "$response" | _egrep_o 'rrset_values.*\[.*\]' | _egrep_o '\[".*\"]') return 0 } _gandi_livedns_rest() { m=$1 ep="$2" data="$3" _debug "$ep" export _H1="Content-Type: application/json" if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then export _H2="Authorization: Bearer $GANDI_LIVEDNS_TOKEN" else export _H2="Authorization: Apikey $GANDI_LIVEDNS_KEY" fi if [ "$m" = "GET" ]; then response="$(_get "$GANDI_LIVEDNS_API/$ep")" else _debug data "$data" response="$(_post "$data" "$GANDI_LIVEDNS_API/$ep" "" "$m")" fi if [ "$?" != "0" ]; then _err "error $ep" return 1 fi _debug2 response "$response" return 0 }
-
Well, did a dirty trick. Still someone should have a look at that script.
Edit /usr/local/pkg/acme/dnsapi/dns_gandi_livedns.sh
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then export _H2="Authorization: Bearer $GANDI_LIVEDNS_TOKEN" else export _H2="Authorization: Apikey $GANDI_LIVEDNS_KEY" fi
to
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then export _H2="Authorization: Bearer $GANDI_LIVEDNS_TOKEN" else export _H2="Authorization: Bearer $GANDI_LIVEDNS_KEY" fi
The script is forced to use the new PAT call now. My certs are up to date again. @Gertjan: Again, thank you for directions.
-
I reported the issue here: Redmine
-
Why not at the source ?
acme.sh is open source, and lives here : https://github.com/acmesh-official/acme.sh.
A bit simplistic, but the core acme.sh script and folder dnsapi are copied 'as is' ones in while into the pfSense source tree.
Netgate itself maintains the GUI wrapper files, and some other scripts "glue" code.If there was an issue with "dns_gandi_livedns.sh" then you find the info neraby here : https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_gandi_livedns.sh - but that file was changed 7 months ago for the last time.
Lets presume you are not the only person using pfSense + acme.sh + "dns_gandi_livedns".
So, this means acme.sh by itself is ok, but something is missing in the pfSense GUI acme.sh glue scripts, also known as 'the pfSense acme.sh package".
This :
"$GANDI_LIVEDNS_TOKEN"
should be set, if needed, in the GUI.
Here :All this is very IMHO of course, I don't use gandi.
I use the very fist DNS API that existed : "rfc2136" or the "nsupdate" method. -
@Gertjan Good point. I linked this thread in the Redmine issue. Possible a UI selection could fix this. Still, I'm no dev and I do not know where everything comes from. I'm also not using Git.