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.
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.
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"
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
- Run the following command to open the crontab editor:
sudo crontab -e
- 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
- If renewal for
--quietoption is skipped, output is suppressed. logger -t certbot-renewrecords the renewal results in the system log. (Verify withjournalctl -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
.envfile permissions to600. (sudo chmod 600 /opt/acme-ncp-dns/.env) - Do not include
.envfiles 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
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."