Netgate Discussion Forum
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Search
    • Register
    • Login

    ACME Gandi.net renewal

    Scheduled Pinned Locked Moved ACME
    8 Posts 2 Posters 415 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • I
      ITSGS_
      last edited by

      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?

      a5f0b933-90e9-4cb9-9c42-031b4d5d142d-image.png

      GertjanG 1 Reply Last reply Reply Quote 0
      • GertjanG
        Gertjan @ITSGS_
        last edited by

        @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.sh

        I don't use Gandi myself, so I can't test.

        Not related, just a question : the certificate is used for a VPN server ?

        No "help me" PM's please. Use the forum, the community will thank you.
        Edit : and where are the logs ??

        I 1 Reply Last reply Reply Quote 1
        • I
          ITSGS_ @Gertjan
          last edited by

          @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. :-)

          I 1 Reply Last reply Reply Quote 0
          • I
            ITSGS_ @ITSGS_
            last edited by ITSGS_

            @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
            }
            
            I 1 Reply Last reply Reply Quote 0
            • I
              ITSGS_ @ITSGS_
              last edited by

              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.

              1 Reply Last reply Reply Quote 0
              • I
                ITSGS_
                last edited by

                I reported the issue here: Redmine

                GertjanG 1 Reply Last reply Reply Quote 0
                • GertjanG
                  Gertjan @ITSGS_
                  last edited by

                  @ITSGS_

                  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 :

                  https://github.com/pfsense/FreeBSD-ports/blob/devel/security/pfSense-pkg-acme/files/usr/local/pkg/acme/acme.inc

                  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.

                  No "help me" PM's please. Use the forum, the community will thank you.
                  Edit : and where are the logs ??

                  I 1 Reply Last reply Reply Quote 1
                  • I
                    ITSGS_ @Gertjan
                    last edited by

                    @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.

                    1 Reply Last reply Reply Quote 0
                    • First post
                      Last post
                    Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.