[GH-ISSUE #1884] Automatic self-signed certificate renewal #1377

Closed
opened 2026-02-26 07:30:44 +03:00 by kerem · 2 comments
Owner

Originally created by @SBado on GitHub (Feb 23, 2022).
Original GitHub issue: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1884

Not really an issue, I just wanted to share my setup for self-signed certificate renewal. I'm using a combination of step-ca, acme.sh and a custom made script to automatically add and renew self signed certificates.

This is the script, I've called it npm-add-certificate:

#!/bin/bash

function usage {
    echo "Usage: npm-add-certificate -n <certificate_name> -c <path_to_certificate> -k <path_to_certificate_key>"
    exit 0
}

if [ $# -eq 0 ]
  then
    usage
    exit 1
fi

while getopts ":hn:c:k:" opt
do
    case "${opt}" in
        n) cert_name=${OPTARG}
           ;;
        c) cert=${OPTARG}
           ;;
        k) cert_key=${OPTARG}
           ;;
        h) usage
           ;;
        :) echo "$0: Must supply an argument to -$OPTARG." >&2
           usage
           exit 2
           ;;
        \?) echo "Invalid option: -${OPTARG}."
           usage
           exit 3
           ;;
        *) usage
           exit 4
           ;;
    esac
done


API="http://<nginx-proxy-manager-ip>:81/api"
IDENTITY='<your-nginx-proxy-manager-username>'
SECRET='<your-nginx-proxy-manager-password>'
TOKEN=""
TOKEN_EXP_DATE=""

function login {
        credentials=$(jq -n --arg id "$IDENTITY" --arg secret "$SECRET" '{ identity: $id, secret: $secret }')
        response=$(curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -d "${credentials}" "${API}/tokens")
        echo ${response} | jq -r --exit-status '.erro' &>/dev/null
        [[ $? -eq 0 ]] && echo "Error: login failed." && exit
        TOKEN="Bearer $(echo $response | jq -r '.token')"
        TOKEN_EXP_DATE=$(echo $response | jq -r '.expires')
}

login

certs=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/certificates?expand=owner")
old_cert_id=$(echo $certs | jq -r '.[] | select(.nice_name=="'"${cert_name}"'") | .id')
[[ $old_cert_id == *$'\n'* ]] && echo "Warning: multiple certs with name \"${cert_name}\" found! Aborting." && exit 5

hosts=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/proxy-hosts")

if [[ ${old_cert_id} != "" ]]
then
        old_cert_hosts=$(echo $hosts | jq -r '[.[] | select(.certificate_id=='"${old_cert_id}"') | .id] | @csv')
        IFS=', ' read -r -a old_cert_hosts <<< "$old_cert_hosts"

        echo "Removing old certificate.."
        delete_result=$(curl -s -X DELETE -H "Authorization: ${TOKEN}" "${API}/nginx/certificates/${old_cert_id}")
        [[ ${delete_result} != "true" ]] && echo "Unable to delete existing certificate." && exit;
fi

echo "Validating new certificate..."
validation_result=$(curl -s -X POST -H "Authorization: ${TOKEN}" -F "certificate=@${cert}" -F "certificate_key=@${cert_key}" "${API}/nginx/certificates/validate")
[[ ${validation_result} == "" ]] && echo "Unable to validate new certificate." && exit;

validation_error=$(echo ${validation_result} | jq -r --exit-status '.error')
[[ $? -eq 0 ]] && echo ${validation_error} && exit

echo ${validation_result} | jq -r --exit-status '.certificate' &>/dev/null
[[ $? -ne 0 ]] && echo "Error: missing certificate." && exit

echo ${validation_result} | jq -r --exit-status '.certificate_key' &>/dev/null
[[ $? -ne 0 ]] && echo "Error: missing certificate key." && exit

echo "Uploading new certificate.."
new_cert=$(curl -s -X POST -H "Authorization: ${TOKEN}" -F "nice_name=${cert_name}" -F 'provider=other' "${API}/nginx/certificates")
new_cert_id=$(echo ${new_cert} | jq -r .id)
curl -s -X POST -H "Authorization: ${TOKEN}" -F "certificate=@${cert}" -F "certificate_key=@${cert_key}" "${API}/nginx/certificates/${new_cert_id}/upload" &>/dev/null


echo "Updating hosts..."
for hid in "${old_cert_hosts[@]}"
do
        host_keys='"domain_names", "forward_scheme", "forward_host", "forward_port", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "block_exploits", "caching_enabled", "allow_websocket_upgrade", "access_list_id", "advanced_config", "enabled", "meta", "locations"'
        old_host=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/proxy-hosts/${hid}")
        # https://stackoverflow.com/a/43354218
        modified_host=$( echo $old_host | jq '. | with_entries(select(.key == ('"${host_keys}"'))) | .certificate_id = '"${new_cert_id}")

        echo "Updating host #${hid}..."
        curl -s -X PUT -H "Authorization: ${TOKEN}" -H 'Content-Type: application/json' -d "${modified_host}" "${API}/nginx/proxy-hosts/${hid}" &>/dev/null
done

echo "Done."

This script will:

  • In case of a new certificate

    • Add the new certificate to NPM
  • In case of a renewed certificate

    • Delete the expired certificate
    • Add the renewed certificate
    • Configure the hosts that were using the now expired certificate to use the renewed one

Assuming you already have a working instance of step-ca (doc) and you already added an ACME provisioner (doc), you can tell acme.sh to work with step (docs) and to call npm-add-certificate every time a certificate is issued and/or renewed.

So for example, if you want to add a certificate for example.mydomain.home, you can issue this command:
acme.sh --issue --standalone -d example.mydomain.home --server https://<yoour-step-ca-instance-hostname>/acme/acme/directory --ca-bundle $HOME/.step/certs/root_ca.crt --keylength ec-256 --post-hook /path/to/npm-acme-hook --renew-hook /path/to/npm-acme-hook

Where npm-acme-hook is a simple script which will call npm-add-certificate with the correct input arguments:
/path/to/npm-add-certificate -n $Le_Domain -c $CERT_FULLCHAIN_PATH -k $CERT_KEY_PATH

You will then find a new self signed certificate called "example.mydomain.home" in Nginx Proxy Manager. This certificate will be automatically renewed by Acme.sh and automatically updated in NPM by npm-add-certificate.
The code is not perfect, I'm not a bash ninja, but for my needs it's usable enough. If you have suggestions or you think you can do better, I'm all ears.

Obligatory warning: never trust a random script by a random guy found on the internet. Before using it, try to understand what it does and do a backup. I will not take responsibility for any harm done to you or your loved ones by this script.

Related issues: #1054 #301 #944, maybe others.

Originally created by @SBado on GitHub (Feb 23, 2022). Original GitHub issue: https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1884 Not really an issue, I just wanted to share my setup for self-signed certificate renewal. I'm using a combination of [step-ca](https://smallstep.com/blog/private-acme-server/), [acme.sh](https://github.com/acmesh-official/acme.sh) and a custom made script to automatically add and renew self signed certificates. This is the script, I've called it npm-add-certificate: ``` #!/bin/bash function usage { echo "Usage: npm-add-certificate -n <certificate_name> -c <path_to_certificate> -k <path_to_certificate_key>" exit 0 } if [ $# -eq 0 ] then usage exit 1 fi while getopts ":hn:c:k:" opt do case "${opt}" in n) cert_name=${OPTARG} ;; c) cert=${OPTARG} ;; k) cert_key=${OPTARG} ;; h) usage ;; :) echo "$0: Must supply an argument to -$OPTARG." >&2 usage exit 2 ;; \?) echo "Invalid option: -${OPTARG}." usage exit 3 ;; *) usage exit 4 ;; esac done API="http://<nginx-proxy-manager-ip>:81/api" IDENTITY='<your-nginx-proxy-manager-username>' SECRET='<your-nginx-proxy-manager-password>' TOKEN="" TOKEN_EXP_DATE="" function login { credentials=$(jq -n --arg id "$IDENTITY" --arg secret "$SECRET" '{ identity: $id, secret: $secret }') response=$(curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -d "${credentials}" "${API}/tokens") echo ${response} | jq -r --exit-status '.erro' &>/dev/null [[ $? -eq 0 ]] && echo "Error: login failed." && exit TOKEN="Bearer $(echo $response | jq -r '.token')" TOKEN_EXP_DATE=$(echo $response | jq -r '.expires') } login certs=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/certificates?expand=owner") old_cert_id=$(echo $certs | jq -r '.[] | select(.nice_name=="'"${cert_name}"'") | .id') [[ $old_cert_id == *$'\n'* ]] && echo "Warning: multiple certs with name \"${cert_name}\" found! Aborting." && exit 5 hosts=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/proxy-hosts") if [[ ${old_cert_id} != "" ]] then old_cert_hosts=$(echo $hosts | jq -r '[.[] | select(.certificate_id=='"${old_cert_id}"') | .id] | @csv') IFS=', ' read -r -a old_cert_hosts <<< "$old_cert_hosts" echo "Removing old certificate.." delete_result=$(curl -s -X DELETE -H "Authorization: ${TOKEN}" "${API}/nginx/certificates/${old_cert_id}") [[ ${delete_result} != "true" ]] && echo "Unable to delete existing certificate." && exit; fi echo "Validating new certificate..." validation_result=$(curl -s -X POST -H "Authorization: ${TOKEN}" -F "certificate=@${cert}" -F "certificate_key=@${cert_key}" "${API}/nginx/certificates/validate") [[ ${validation_result} == "" ]] && echo "Unable to validate new certificate." && exit; validation_error=$(echo ${validation_result} | jq -r --exit-status '.error') [[ $? -eq 0 ]] && echo ${validation_error} && exit echo ${validation_result} | jq -r --exit-status '.certificate' &>/dev/null [[ $? -ne 0 ]] && echo "Error: missing certificate." && exit echo ${validation_result} | jq -r --exit-status '.certificate_key' &>/dev/null [[ $? -ne 0 ]] && echo "Error: missing certificate key." && exit echo "Uploading new certificate.." new_cert=$(curl -s -X POST -H "Authorization: ${TOKEN}" -F "nice_name=${cert_name}" -F 'provider=other' "${API}/nginx/certificates") new_cert_id=$(echo ${new_cert} | jq -r .id) curl -s -X POST -H "Authorization: ${TOKEN}" -F "certificate=@${cert}" -F "certificate_key=@${cert_key}" "${API}/nginx/certificates/${new_cert_id}/upload" &>/dev/null echo "Updating hosts..." for hid in "${old_cert_hosts[@]}" do host_keys='"domain_names", "forward_scheme", "forward_host", "forward_port", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "block_exploits", "caching_enabled", "allow_websocket_upgrade", "access_list_id", "advanced_config", "enabled", "meta", "locations"' old_host=$(curl -s -H 'Accept: application/json' -H "Authorization: ${TOKEN}" "${API}/nginx/proxy-hosts/${hid}") # https://stackoverflow.com/a/43354218 modified_host=$( echo $old_host | jq '. | with_entries(select(.key == ('"${host_keys}"'))) | .certificate_id = '"${new_cert_id}") echo "Updating host #${hid}..." curl -s -X PUT -H "Authorization: ${TOKEN}" -H 'Content-Type: application/json' -d "${modified_host}" "${API}/nginx/proxy-hosts/${hid}" &>/dev/null done echo "Done." ``` This script will: - In case of a new certificate - Add the new certificate to NPM - In case of a renewed certificate - Delete the expired certificate - Add the renewed certificate - Configure the hosts that were using the now expired certificate to use the renewed one Assuming you already have a working instance of step-ca ([doc](https://smallstep.com/docs/step-ca/getting-started)) and you already added an ACME provisioner ([doc](https://smallstep.com/docs/tutorials/acme-challenge#acme-with-open-source-step-ca)), you can tell acme.sh to work with step ([docs](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients/#acmesh)) and to call npm-add-certificate every time a certificate is issued and/or renewed. So for example, if you want to add a certificate for example.mydomain.home, you can issue this command: `acme.sh --issue --standalone -d example.mydomain.home --server https://<yoour-step-ca-instance-hostname>/acme/acme/directory --ca-bundle $HOME/.step/certs/root_ca.crt --keylength ec-256 --post-hook /path/to/npm-acme-hook --renew-hook /path/to/npm-acme-hook` Where npm-acme-hook is a simple script which will call npm-add-certificate with the correct input arguments: `/path/to/npm-add-certificate -n $Le_Domain -c $CERT_FULLCHAIN_PATH -k $CERT_KEY_PATH` You will then find a new self signed certificate called "example.mydomain.home" in Nginx Proxy Manager. This certificate will be automatically renewed by Acme.sh and automatically updated in NPM by npm-add-certificate. The code is not perfect, I'm not a bash ninja, but for my needs it's usable enough. If you have suggestions or you think you can do better, I'm all ears. Obligatory warning: never trust a random script by a random guy found on the internet. Before using it, try to understand what it does and **do a backup**. I will not take responsibility for any harm done to you or your loved ones by this script. Related issues: #1054 #301 #944, maybe others.
kerem 2026-02-26 07:30:44 +03:00
  • closed this issue
  • added the
    stale
    label
Author
Owner

@github-actions[bot] commented on GitHub (Feb 23, 2024):

Issue is now considered stale. If you want to keep it open, please comment 👍

<!-- gh-comment-id:1960626193 --> @github-actions[bot] commented on GitHub (Feb 23, 2024): Issue is now considered stale. If you want to keep it open, please comment :+1:
Author
Owner

@github-actions[bot] commented on GitHub (Apr 8, 2025):

Issue was closed due to inactivity.

<!-- gh-comment-id:2785047625 --> @github-actions[bot] commented on GitHub (Apr 8, 2025): Issue was closed due to inactivity.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/nginx-proxy-manager-NginxProxyManager#1377
No description provided.