Available in Classic and VPC
This guide describes how to use Certbot to automate certificate issuance and renewal in a Linux server environment.
It was composed based on the Ubuntu 22.04/24.04 LTS and Rocky Linux 8/RHEL 8 environments.
Ncloud Trust CA only accepts RSA 2048. Certificate issuance will fail if requested with ECDSA or RSA 4096. The commands in the guide below include the --key-type rsa --rsa-key-size 2048 option. Do not change this arbitrarily.
This guide is based on Certbot. Other RFC 8555 compliant ACME clients may be used, but official technical support is provided only based on Certbot.
Before getting started
Before following this guide, complete all the steps in ACME prerequisites. You must carry out the following in advance:
- Be issued EAB Key ID and EAB HMAC Key.
- Determine which domain validation method to use (DNS-01 dynamic method or pre-validation method).
- If using an OV certificate, complete organizational validation in Certificate Manager > Organization.
Select domain validation method
Before issuing a certificate with Certbot, you must decide how to perform Domain Control Validation (DCV). Ncloud Trust CA supports two methods, and the preparation tasks and issuance commands differ depending on which method you select.
| Type | DNS-01 dynamic method | Pre-validation method |
|---|---|---|
| DCV upon issuance | Validate with _acme-challenge TXT records whenever issuing or renewing. |
Domain pre-validation is completed once during registration and is omitted at the time of issuance. |
| Hook script | Required (--manual-auth-hook / --manual-cleanup-hook). |
Not required. |
| Preparations | Configure DNS API integration hook script (Step 2). | Register domain pre-validation via console / Open API. |
| Recommended authenticator | --manual + Hook. |
--standalone |
| Suitability | Environments where DNS can be automatically controlled via API; wildcard and multiple domains. | Environments where it is difficult to configure a DNS hook or where pre-validation has already been used. |
Proceed with the subsequent steps as follows, depending on the validation method:
- DNS-01 dynamic method: Step 1 (Installation) → Step 2 (Hook script configuration) → Step 3A (Dynamic method issuance) → Step 4 (Automatic renewal).
- Pre-validation method: Step 1 (Installation) → Skip Step 2 → Step 3B (Pre-validation method issuance) → Step 4 (Automatic renewal).
Step 1: Install Certbot
Install Certbot by running the right commands for your operating system environment. For detailed installation instructions, see the Certbot official 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 installation is complete, check the version using the following commands: We recommend Certbot 2.x or higher.
certbot --version
jq --version
openssl version
Step 2: Configure DNS hook script
This step is only for the DNS-01 dynamic method. If you are using the pre-validation method, this step is unnecessary, so skip it and proceed to Step 3: Issue certificate method B.
To automate the DNS-01 challenge, integrate a script to create and delete DNS TXT records with Certbot's --manual-auth-hook and --manual-cleanup-hook options.
Configure the task directory as follows:
/opt/acme-ncp-dns/
├── .env # Configuration file (create directly, do not expose 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 examples for hook script examples. If you are using a different DNS provider, write a script that performs the same role by calling that provider's API.
After placing the script file, create a configuration file as follows and grant 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
.env file content:
# Ncloud API authentication key (In the console, My Account > Account and security management > Security management > Access management > API authentication key)
NCP_ACCESS_KEY="YOUR_NCP_ACCESS_KEY"
NCP_SECRET_KEY="YOUR_NCP_SECRET_KEY"
# NCP Global DNS API endpoint (no need to change)
NCP_DNS_API="https://globaldns.apigw.ntruss.com"
# Global DNS domain ID
# NCP Console > Global DNS > F12 Developer Tools > Click on the domain > URL number (Example: dns/domain/36019)
NCP_DOMAIN_ID="YOUR_DOMAIN_ID"
# Root domain registered in Global DNS (Example: example.com)
NCP_ZONE_DOMAIN="example.com"
# DNS TXT record dissemination waiting time (sec). If validation fails, increase to 90–120.
DNS_PROPAGATION_SECONDS=60
| Item | Description | How to check |
|---|---|---|
NCP_ACCESS_KEY |
Ncloud API Access Key | In the console, My Account > Account and security management > Security management > Access management > API authentication key. |
NCP_SECRET_KEY |
Ncloud API Secret Key | In the console, My Account > Account and security management > Security management > Access management > API authentication key. |
NCP_DOMAIN_ID |
Global DNS domain number ID | In the console, Global DNS > F12 > check URL number. |
NCP_ZONE_DOMAIN |
DNS component root domain | Domain registered in Global DNS (Example: example.com). |
DNS_PROPAGATION_SECONDS |
DNS dissemination waiting time | Default: 60 sec. Adjust to 90–120 if validation fails. |
Step 3: Issue certificate
Depending on the selected domain validation method, proceed with either Method A (DNS-01 dynamic method) or Method B (pre-validation method). Both methods use the common parameters below. Execute by replacing the items in square brackets ([ ]) with the actual values.
| Parameter | Description | Example |
|---|---|---|
[ACME_DIRECTORY_URL] |
ACME directory URL confirmed in Preparing to start ACME. | https://acme.navercloudtrust.com/acme/directory |
[EAB_KEY_ID] |
Key ID provided when issuing EAB credentials. | abc123... |
[EAB_HMAC_KEY] |
HMAC Key provided when issuing EAB credentials. | xyz789... |
[ADMIN_EMAIL] |
Email address to receive certificate expiration notifications. | admin@example.com |
--eab-kid / --eab-hmac-key is used only once when the ACME account is first registered on the server. Once the account is registered, it does not need to be specified again upon renewal.
Method A: DNS-01 dynamic method
Proceed after configuring the hook script in Step 2. Use the --manual authenticator and the hook script to automatically create, validate, and delete DNS TXT records at the time of issuance.
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
Including sub-domain (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 you include a wildcard (*), enclose the domain in quotes to prevent the shell from expanding globs.
Method B: Pre-validation method
When you register a domain for pre-validation via the console or Open API, the DCV is completed once at that point. In this case, the ACME server responds to the issuance request with order/authz in a validated state from the start, so Certbot skips the DNS challenge/hook step entirely. Therefore, neither a hook script nor a web root path is required.
However, since Certbot's structure requires specifying at least one authenticator (domain validation method) plugin, the simplest approach with the pre-validation method where no actual challenge occurs is to specify --standalone "formally." In the pre-validation state, --standalone does not actually open a temporary web server or port.
The operation flow of the pre-validation method is as follows:
[Preparations] DCV has already been completed once when pre-validating the domain via the console/Open API
──────────────────────────────────────────────────────────
1. Register account (EAB binding)
2. newOrder → order immediately "ready", authz "valid" (responds with validation complete status)
3. Certbot confirms that authz is already valid → Skip entire challenge/hook step
(Do not create TXT, do not call auth-hook/cleanup-hook)
4. Finalize (Submit CSR)
5. Server processing → Valid → Download certificate
Single domain
sudo certbot certonly \
--standalone \
--key-type rsa \
--rsa-key-size 2048 \
--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
Including sub-domain (SAN)
sudo certbot certonly \
--standalone \
--key-type rsa \
--rsa-key-size 2048 \
--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
All domains (and wildcards) specified by -d must be pre-validated. If domains that have not been pre-validated are included, the corresponding authz will not give a response of valid, and issuance will fail.
Once issuance has succeeded, verify the certificate using the following commands: (This applies regardless of the validation method.)
sudo certbot certificates
sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem \
-noout -subject -issuer -dates
Step 4: Configure automatic renewal
Certbot will start attempting to renew the certificate from 30 days before expiration. Since the options used during issuance (validation method, hook script path, etc.) are automatically saved, only certbot renew has to be executed to carry out the renewal in the same manner as the initial issuance.
- DNS-01 dynamic method: When renewing, DNS validation is carried out automatically via the saved hook path.
- Pre-validation method: As long as the pre-validation state is maintained, the hook is not called even on renewal, and renewal is carried out immediately via
--standalone.
First, simulate and verify the renewal operation using the following command:
sudo certbot renew --dry-run
Method 1: systemd timer (recommended)
If you installed Certbot via snap, the renewal timer is automatically registered. Check the 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
- Execute the following command to open the crontab editor:
sudo crontab -e
- Add the following line: (Renewal is attempted every day at 3:00 AM.)
0 3 * * * /usr/bin/certbot renew --quiet 2>&1 | logger -t certbot-renew
- When skipping without renewing, the
--quietoptions suppresses output. logger -t certbot-renewrecords the renewal results in the system log. (Check withjournalctl -t certbot-renew.)- Renewal logs can also be viewed in
/var/log/letsencrypt/letsencrypt.log.
Apply web server certificate
The locations of the issued certificate files are as follows:
| File | Description |
|---|---|
/etc/letsencrypt/live/<도메인>/cert.pem |
Certificate |
/etc/letsencrypt/live/<도메인>/chain.pem |
Middle CA chain |
/etc/letsencrypt/live/<도메인>/fullchain.pem |
Certificate + chain (web server configuration recommended) |
/etc/letsencrypt/live/<도메인>/privkey.pem |
Personal key (do not expose externally) |
Nginx
Enter the following content into the /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 for automatic reloading 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
Enter the following content into the /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 | Solution |
|---|---|---|
The key ID was not found |
An EAB Key ID error or key has already been used. | Reissue the EAB key via the console and execute the command again. |
unauthorized |
EAB HMAC Key error. | Check the HMAC Key value again. Check for blank spaces or line breaks. |
Error finalizing order :: invalid CSR or Unsupported key type or size. Only RSA-2048 is accepted. |
Key type other than RSA 2048 used. | Check that the --key-type rsa --rsa-key-size 2048 option was added to the commands. |
DNS problem: NXDOMAIN |
TXT record not disseminated (dynamic method). | Either increase the DNS_PROPAGATION_SECONDS value in .env to 90–120, or confirm dissemination using dig +short TXT _acme-challenge.example.com @8.8.8.8. |
| TXT record creation fails | Ncloud API key error or domain ID mismatch (dynamic method). | Check the NCP_ACCESS_KEY, NCP_SECRET_KEY, and NCP_DOMAIN_ID values in .env again. |
Could not choose an appropriate plugin: authenticator |
authenticator not specified. | For the dynamic method, specify --manual; for the pre-validation method, specify --standalone. |
| Despite using the pre-validation method, issuance fails (authz does not respond as valid) | Domains that have not been pre-validated included in -d. |
Check the pre-validation status of the relevant domains in the console/Open API. |
| OV certificate issuance fails | Organizational pre-validation incomplete. | Check validation status in Certificate Manager > Organization. |
| Hook does not run during renewal (dynamic method) | Hook path missing from renewal configuration file. | Directly add the paths for manual_auth_hook and manual_cleanup_hook to the [renewalparams] section of /etc/letsencrypt/renewal/example.com.conf. |
sudo tail -100 /var/log/letsencrypt/letsencrypt.log
Security recommendations
- Make sure to set the permissions of the
.envfile to600. (sudo chmod 600 /opt/acme-ncp-dns/.env) - Do not include
.envfiles in Git repositories or shared folders. - For Ncloud API keys, we recommend granting the minimum permissions only to the Global DNS service.
- Take care not to expose certificate personal keys (
privkey.pem) externally.
Ncloud Global DNS hook script examples
The scripts below are reference examples for the Ncloud Global DNS environment. You are responsible for any modifications, configurations, execution environments, and results of the script, and NAVER Cloud does not provide technical support for its operation. We recommend conducting sufficient testing before applying it to an actual operation 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
# Prevent editor parsing errors by removing nl variable ($) assignment and handling line breaks with printf
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 wildcards
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] TXT record creation has failed: ${RESPONSE}" >&2
exit 1
fi
echo "${SID}" > "/tmp/ncp_sid_${CERTBOT_DOMAIN}.txt"
# Reflect 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 created. Waiting ${DNS_PROPAGATION_SECONDS} seconds..."
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 you are trying to delete could not be found." >&2
exit 0
fi
# Delete TXT record (Forward 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
# Reflect 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 deleted."