Documentation Index

Fetch the complete documentation index at: https://guide.ncloud-docs.com/llms.txt

Use this file to discover all available pages before exploring further.

Automate certificate issuance on Linux servers based on Certbot

Prev Next

Available in Classic and VPC

This guide explains how to use Certbot to automate certificate issuance and renewal in a Linux server environment.
This guide is prepared based on Ubuntu 22.04/24.04 LTS and Rocky Linux 8/RHEL 8 environments.

Caution

Ncloud Trust CA only accepts RSA 2048. If you request an ECDSA or RSA 4096 certificate, the issuance will fail. The commands in the guide below include --key-type rsa --rsa-key-size 2048 option. Do not change this arbitrarily.

Note

This guide is based on Certbot. Other ACME clients that comply with RFC 8555 are also available, but official technical support is provided only for Certbot.

Before you start

Before proceeding with this guide, complete all the steps in ACME prerequisites. The following items must be prepared in advance.

  • Issued EAB Key ID and EAB HMAC Key
  • Determine the domain validation method to use (DNS-01 dynamic method or pre-validation method)
  • When using an OV certificate, complete organization validation under Certificate Manager > Organization

Step 1: Install Certbot

Install Certbot by running the command appropriate for your operating system. For detailed installation instructions, see the Official Certbot installation guide.

Ubuntu 22.04 / 24.04 LTS

sudo apt update
sudo apt install -y curl openssl jq python3
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Rocky Linux 8 / RHEL 8

sudo dnf install -y epel-release
sudo dnf install -y curl openssl jq python3 snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
# Run after logging in again
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Once the installation is complete, check the version using the following command: Certbot version 2.x or higher is recommended.

certbot --version
jq --version
openssl version

Step 2: Configure the DNS hook script

To automate the DNS-01 challenge, integrate a script that creates and deletes DNS TXT records with Certbot’s --manual-auth-hook and --manual-cleanup-hook options.

The working directory is composed of as follows:

/opt/acme-ncp-dns/
├── .env            # Configuration File (Create manually; Do not disclose externally)
├── ncp-auth.sh     # Certbot authentication hook (Create TXT record)
└── ncp-cleanup.sh  # Certbot cleanup hook (Delete TXT record)

Create a directory.

sudo mkdir -p /opt/acme-ncp-dns

If you are using Ncloud Global DNS, see Ncloud Global DNS hook script example for sample hook scripts. If you are using a different DNS provider, write a script that calls that provider's API to perform the same function.

After placing the script file, create a configuration file as shown below and grant it execution permissions.

sudo vi /opt/acme-ncp-dns/.env
sudo chmod 600 /opt/acme-ncp-dns/.env
sudo chmod +x /opt/acme-ncp-dns/ncp-auth.sh
sudo chmod +x /opt/acme-ncp-dns/ncp-cleanup.sh

Contents of .env file:

# Ncloud API authentication key (My Account > Account and security management > Security management > Access management > API authentication key menu on the console.)
NCP_ACCESS_KEY="YOUR_NCP_ACCESS_KEY"
NCP_SECRET_KEY="YOUR_NCP_SECRET_KEY"

# NCP Global DNS API endpoints (no need to change)
NCP_DNS_API="https://globaldns.apigw.ntruss.com"

# Global DNS Domain ID
# NCP Console > Global DNS > F12 Developer Tools > Click Domain > The number in the URL (e.g., dns/domain/36019)
NCP_DOMAIN_ID="YOUR_DOMAIN_ID"

# Root domain registered in the Global DNS (e.g., example.com)
NCP_ZONE_DOMAIN="example.com"

# DNS TXT record propagation delay (seconds). If validation fails, increase it to 90–120.
DNS_PROPAGATION_SECONDS=60
Item Description How to check
NCP_ACCESS_KEY Ncloud API Access Key My Account > Account and security management > Security management > Access management > API authentication key menu on the console.
NCP_SECRET_KEY Ncloud API Secret Key My Account > Account and security management > Security management > Access management > API authentication key menu on the console.
NCP_DOMAIN_ID Global DNS domain number ID Check the URL number in the console under Global DNS > F12
NCP_ZONE_DOMAIN DNS component root domain Domain registered in the Global DNS (e.g., example.com)
DNS_PROPAGATION_SECONDS DNS propagation delay Default: 60 seconds. If validation fails, increase it to 90–120.

Step 3: Issue certificate

Replace the item in parentheses ([ ]) in the following command with the actual value and run it.

Parameter Description Example
[ACME_DIRECTORY_URL] The ACME directory URL verified during the ACME setup https://acme.navercloudtrust.com/acme/directory
[EAB_KEY_ID] Key ID provided upon issuance of the EAB credentials abc123...
[EAB_HMAC_KEY] HMAC Key provided upon issuance of the EAB credentials xyz789...
[ADMIN_EMAIL] Email address to receive certificate expiration notifications admin@example.com

Single domain

sudo certbot certonly \
  --manual \
  --preferred-challenges dns \
  --key-type rsa \
  --rsa-key-size 2048 \
  --manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
  --manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
  --server [ACME_DIRECTORY_URL] \
  --eab-kid "[EAB_KEY_ID]" \
  --eab-hmac-key "[EAB_HMAC_KEY]" \
  --agree-tos \
  --email [ADMIN_EMAIL] \
  --non-interactive \
  -d example.com

Include subdomains (SAN)

sudo certbot certonly \
  --manual \
  --preferred-challenges dns \
  --key-type rsa \
  --rsa-key-size 2048 \
  --manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
  --manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
  --server [ACME_DIRECTORY_URL] \
  --eab-kid "[EAB_KEY_ID]" \
  --eab-hmac-key "[EAB_HMAC_KEY]" \
  --agree-tos \
  --email [ADMIN_EMAIL] \
  --non-interactive \
  -d example.com \
  -d www.example.com \
  -d api.example.com

Wildcard domain

sudo certbot certonly \
  --manual \
  --preferred-challenges dns \
  --key-type rsa \
  --rsa-key-size 2048 \
  --manual-auth-hook /opt/acme-ncp-dns/ncp-auth.sh \
  --manual-cleanup-hook /opt/acme-ncp-dns/ncp-cleanup.sh \
  --server [ACME_DIRECTORY_URL] \
  --eab-kid "[EAB_KEY_ID]" \
  --eab-hmac-key "[EAB_HMAC_KEY]" \
  --agree-tos \
  --email [ADMIN_EMAIL] \
  --non-interactive \
  -d "*.example.com"
Note

If your domain contains a wildcard (*), enclose it in quotes to prevent the shell from applying its glob expansion.

After the certificate has been successfully issued, verify it using the following command:

sudo certbot certificates
sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem \
  -noout -subject -issuer -dates

Step 4: Set automatic renewal

Certbot attempts to renew the certificate from 30 days before it expires. Since the hook script path is automatically saved when the certificate is first issued, running certbot renew will automatically perform DNS validation.

First, use the following command to simulate the renewal process and verify it:

sudo certbot renew --dry-run

Method 1: systemd timer (recommended)

If you installed Certbot using snap, the renewal timer is automatically registered. Check its status using the following command:

sudo systemctl status snap.certbot.renew.timer

# Register manually if there is no timer.
sudo systemctl enable --now snap.certbot.renew.timer

Method 2: Crontab

  1. Run the following command to open the crontab editor:
sudo crontab -e
  1. Add the following line: (Attempt to renew every day at 3 a.m.)
0 3 * * * /usr/bin/certbot renew --quiet 2>&1 | logger -t certbot-renew
Note
  • If renewal for --quiet option is skipped, output is suppressed.
  • logger -t certbot-renew records the renewal results in the system log. (Verify with journalctl -t certbot-renew.)
  • You can also view the renewal log in /var/log/letsencrypt/letsencrypt.log.

Apply web server certificate

The location of the issued certificate file is as follows:

File Description
/etc/letsencrypt/live/<도메인>/cert.pem Certificate
/etc/letsencrypt/live/<도메인>/chain.pem Intermediate CA chain
/etc/letsencrypt/live/<도메인>/fullchain.pem Certificate + Chain (recommended for web server configuration)
/etc/letsencrypt/live/<도메인>/privkey.pem Private key (Do not disclose externally)

Nginx

Add the following content to /etc/nginx/sites-available/example.com file.

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:10m;

    location / {
        root /var/www/html;
        index index.html;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
sudo nginx -t
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo systemctl reload nginx

Configure a deploy-hook to automatically reload after a renewal.

sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Apache

Add the following content to /etc/apache2/sites-available/example.com-ssl.conf file. (Based on Ubuntu)

<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile      /etc/letsencrypt/live/example.com/cert.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/example.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

    SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
    SSLHonorCipherOrder     off

    DocumentRoot /var/www/html
</VirtualHost>
sudo a2enmod ssl
sudo a2ensite example.com-ssl
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
#!/bin/bash
systemctl reload apache2
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh

Troubleshooting

Symptom Cause Action
The key ID was not found EAB Key ID error or key already in use Re-run the command after reissuing the EAB key in the console.
unauthorized EAB HMAC key error Verify the HMAC key value again. Verify whether spaces and line breaks are included.
Error finalizing order :: invalid CSR Use key types other than RSA 2048. Verify whether --key-type rsa --rsa-key-size 2048 option is added to the command.
DNS problem: NXDOMAIN TXT record not propagated Increase DNS_PROPAGATION_SECONDS value of .env to 90–120 or verify whether it propagates to dig +short TXT _acme-challenge.example.com @8.8.8.8.
TXT record not created Ncloud API key error or Domain ID mismatch Verify NCP_ACCESS_KEY, NCP_SECRET_KEY and NCP_DOMAIN_ID values of .env again.
OV certificate not issued Organization pre-validation not completed Check the validation status in Certificate Manager > Organization.
The hook does not run during a renewal. Missing hook path in the renewal configuration file In [renewalparams] section of /etc/letsencrypt/renewal/example.com.conf, add manual_auth_hook and manual_cleanup_hook paths directly.
sudo tail -100 /var/log/letsencrypt/letsencrypt.log

Security advisory

  • Be sure to set .env file permissions to 600. (sudo chmod 600 /opt/acme-ncp-dns/.env)
  • Do not include .env files in a Git repository or shared folder.
  • We recommend granting the Ncloud API key only the minimum permissions limited to the Global DNS service.
  • Take care to ensure that the certificate's private key (privkey.pem) is not exposed to outside parties.

Ncloud Global DNS hook script example

Caution

The following script is provided as an example for reference in the Ncloud Global DNS environment. The user is responsible for modifying, configuring, and running the script, as well as for its results. NAVER Cloud does not provide technical support for the operation of the script. We recommend conducting thorough testing before implementing this in a live production environment.

ncp-auth.sh

#!/bin/bash
# ncp-auth.sh — Certbot DNS-01 authentication hook (NCP Global DNS)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/.env"

make_signature() {
  local method=$1 uri=$2 timestamp=$3
  # Remove the assignment of nl variable ($) and use printf to handle line breaks to prevent editor parsing errors.
  printf "%s %s\n%s\n%s" "${method}" "${uri}" "${timestamp}" "${NCP_ACCESS_KEY}" \
    | openssl dgst -sha256 -hmac "${NCP_SECRET_KEY}" -binary | base64
}

# Calculate TXT host after removing wildcard
CLEAN_DOMAIN=$(echo "${CERTBOT_DOMAIN}" | sed 's/^\*\.//')
if [ "${CLEAN_DOMAIN}" = "${NCP_ZONE_DOMAIN}" ]; then
  HOST="_acme-challenge"
else
  SUB="${CLEAN_DOMAIN%.${NCP_ZONE_DOMAIN}}"
  HOST="_acme-challenge.${SUB}"
fi

# Create TXT record
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI="/dns/v1/ncpdns/record/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "POST" "${URI}" "${TIMESTAMP}")

RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 -X POST \
  "${NCP_DNS_API}${URI}" \
  -H "Content-Type: application/json" \
  -H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
  -H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
  -H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
  -d "[{\"host\":\"${HOST}\",\"type\":\"TXT\",\"content\":\"${CERTBOT_VALIDATION}\",\"ttl\":300,\"lbRegionCode\":\"KR\"}]")

SID=$(echo "${RESPONSE}" | jq -r '.[0].sid // empty')
if [ -z "${SID}" ]; then
  echo "[Error] Failed to create TXT record: ${RESPONSE}" >&2
  exit 1
fi

echo "${SID}" > "/tmp/ncp_sid_${CERTBOT_DOMAIN}.txt"

# Apply changes
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI_APPLY="/dns/v1/ncpdns/record/apply/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "PUT" "${URI_APPLY}" "${TIMESTAMP}")

curl -s --connect-timeout 10 --max-time 30 -X PUT \
  "${NCP_DNS_API}${URI_APPLY}" \
  -H "Content-Type: application/json" \
  -H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
  -H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
  -H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
  -d '{}' > /dev/null

echo "DNS TXT record creation complete. Waiting for ${DNS_PROPAGATION_SECONDS} second(s)..."
sleep "${DNS_PROPAGATION_SECONDS}"

ncp-cleanup.sh

#!/bin/bash
# ncp-cleanup.sh — Certbot DNS-01 cleanup hook (NCP Global DNS)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/.env"

make_signature() {
  local method=$1 uri=$2 timestamp=$3
  printf "%s %s\n%s\n%s" "${method}" "${uri}" "${timestamp}" "${NCP_ACCESS_KEY}" \
    | openssl dgst -sha256 -hmac "${NCP_SECRET_KEY}" -binary | base64
}

TMP_FILE="/tmp/ncp_sid_${CERTBOT_DOMAIN}.txt"
SID=$(cat "${TMP_FILE}" 2>/dev/null || true)
if [ -z "${SID}" ]; then
  echo "[Warning] The record ID to be deleted cannot be found." >&2
  exit 0
fi

# Delete TXT record (pass the SID as an array in the request body)
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI="/dns/v1/ncpdns/record/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "DELETE" "${URI}" "${TIMESTAMP}")

curl -s --connect-timeout 10 --max-time 30 -X DELETE \
  "${NCP_DNS_API}${URI}" \
  -H "Content-Type: application/json" \
  -H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
  -H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
  -H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
  -d "[${SID}]" > /dev/null

# Apply changes
TIMESTAMP=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null || date +%s000)
URI_APPLY="/dns/v1/ncpdns/record/apply/${NCP_DOMAIN_ID}"
SIGNATURE=$(make_signature "PUT" "${URI_APPLY}" "${TIMESTAMP}")

curl -s --connect-timeout 10 --max-time 30 -X PUT \
  "${NCP_DNS_API}${URI_APPLY}" \
  -H "Content-Type: application/json" \
  -H "x-ncp-apigw-timestamp: ${TIMESTAMP}" \
  -H "x-ncp-iam-access-key: ${NCP_ACCESS_KEY}" \
  -H "x-ncp-apigw-signature-v2: ${SIGNATURE}" \
  -d '{}' > /dev/null

rm -f "${TMP_FILE}"
echo "DNS TXT record deletion complete."