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.

Certbot-based Linux server certificate automation

Prev Next

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.

Caution

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.

Note

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

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

Note

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
Note

--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"
Note

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.

Note

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
Caution

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

  1. Execute the following command to open the crontab editor:
sudo crontab -e
  1. 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
Note
  • When skipping without renewing, the --quiet options suppresses output.
  • logger -t certbot-renew records the renewal results in the system log. (Check with journalctl -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 .env file to 600. (sudo chmod 600 /opt/acme-ncp-dns/.env)
  • Do not include .env files 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

Caution

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