--- title: "Certbot-based Linux server certificate automation" slug: "certificate-linuxacme" updated: 2026-09-17T09:20:41Z published: 2026-09-17T09:20:41Z canonical: "guide.ncloud-docs.com/certificate-linuxacme" stale: true --- > ## 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 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](/docs/en/certificatemanager-acme-prepare). 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](https://certbot.eff.org/instructions). **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](/docs/en/certificate-linuxacme#3%EB%8B%A8%EA%B3%84%EC%9D%B8%EC%A6%9D%EC%84%9C%EB%B0%9C%EA%B8%89) **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](/docs/en/certificate-linuxacme#NcloudGlobalDNS%ED%9B%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%98%88%EC%8B%9C) 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](/docs/en/certificate-linuxacme#2%EB%8B%A8%EA%B3%84DNS%ED%9B%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%84%A4%EC%A0%95). 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.) ``` 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 ``` ``` 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." ```