This tutorial installs a Sectigo or TrustSign SSL certificate ordered on HTTPCS with acme.sh, the reference ACME client on Linux. acme.sh obtains the certificate, installs it and then renews it automatically.
First choose your certificate type: the prerequisites, commands and checks adapt to it. Then enter your details: they are inserted into the commands, ready to copy. Each step shows the expected result and the common errors.
Your choices adapt the prerequisites, commands and checks in the rest of the tutorial.
sudo -i.curl and crontab commands installed (Debian or Ubuntu: apt install curl cron)./root/.acme.sh. Do not run the installation script again: the tutorial only adds what this certificate needs.*.mydomain.tld wildcard covers direct subdomains (blog.mydomain.tld), not deeper levels (a.blog.mydomain.tld). The bare domain mydomain.tld is added to the commands automatically.The values you enter replace the areas highlighted in yellow in the commands. A filled-in area turns blue. Clicking a yellow area takes you to the matching field.
Nothing is sent or stored: the commands are generated in your browser. All fields are optional.
Server url value from your dashboard. Type “sectigo” or “trust” to find it.
This URL belongs to an Organization (OV) certificate, but you chose Domain (DV). Use the URL shown in your dashboard.
This URL belongs to a Domain (DV) certificate, but you chose Organization (OV). Use the URL shown in your dashboard.
ACME account contact, used for expiry notices.
kid value from the dashboard: the short identifier.
hmac value from the dashboard: the long key. Do not share it.
Without http:// or path, for example mydomain.tld.
One domain per line, as they appear in your order.
Enter the domain without *.: the commands request both mydomain.tld and *.mydomain.tld.
Folder served for this domain: root directive in Nginx, DocumentRoot in Apache.
In Cloudflare, My Profile › API Tokens › Create Token, “Edit zone DNS” template restricted to this domain. The Zone ID is on the Overview page of the domain.
Create the three keys at once on the page of your region: eu.api.ovh.com/createToken ca.api.ovh.com/createToken api.us.ovhcloud.com/createToken eu.api.kimsufi.com/createToken ca.api.kimsufi.com/createToken eu.api.soyoustart.com/createToken ca.api.soyoustart.com/createToken
Option 1, every zone of the account (prefilled by the link):
/domain/zone/*
Option 2, this zone only: declare both paths.
/domain/zone/mydomain.tld
/domain/zone/mydomain.tld/*
The page then shows the Application Key, the Application Secret and the Consumer Key: enter them in the matching fields.
In Gandi, User settings › Personal Access Tokens, with the right to manage the technical configuration of domains.
In the Infomaniak Manager, Developer › API tokens: create a token with the domain scope.
Create a key on developer.hosting.ionos.com/keys: IONOS shows a public prefix and a secret, to enter in two separate fields.
In the Scaleway console, IAM › API keys: use the Secret Key of a key whose policy includes this permission set:
DomainsDNSFullAccess
In AWS IAM, create an access key for a user allowed to perform these actions:
route53:ListHostedZones
route53:ChangeResourceRecordSets
route53:GetChange
More than 150 providers are supported: see the acme.sh documentation.
Command that reloads your software so it reads the new certificate.
Name given in the acme.sh documentation for your provider, for example dns_gd.
The script installs acme.sh in /root/.acme.sh, adds the acme.sh alias and a renewal cron job. The last command prints the installed version.
curl https://get.acme.sh | sh -s email=admin@mydomain.tld
source ~/.bashrc
acme.sh --version
acme.sh: command not found
The alias is not loaded yet: run source ~/.bashrc or open a new session. Also check that the installation was done by the same user (root).
Pre-check failed
The crontab command is missing. Install it (apt install cron) and run the installation again.
curl: (6) Could not resolve host or timeout
The server has no outbound Internet access. Check its DNS resolution and outbound firewall: github.com must be reachable.
Update acme.sh and list the certificates it already manages. The rest of the tutorial does not change them.
acme.sh --upgrade
acme.sh --list
Each HTTPCS order has its own ACME account, while acme.sh keeps only one account per server URL. Check whether an account already exists for the URL of this certificate:
ls /root/.acme.sh/ca/SERVER_URL/account.key
No such file or directory: no account, keep the shared configuration. The account.key file is listed: another certificate already uses this URL, choose the separate configuration, otherwise acme.sh would reuse the old account and the ACME server would reject this domain.
List the DNS API credentials already saved, without showing their values:
grep -o '^SAVED_[A-Za-z_]*' /root/.acme.sh/account.conf
Variables of another provider (for example SAVED_OVH_AK while this domain is at IONOS) are not a problem: each provider has its own variables. If the variables of your provider are listed for another account than the one of this domain, choose the separate configuration: exporting new values would replace the old ones and renewals of the other certificates would fail.
acme.sh: command not found
acme.sh is installed for another user or in another folder. Find it with ls -d /root/.acme.sh /home/*/.acme.sh and work as that same user.
acme.sh --list does not show the known certificates
They belong to another configuration. The cron line tells which one: crontab -l | grep acme.sh (--config-home option).
This command links acme.sh to your HTTPCS ACME account with the EAB (External Account Binding) credentials. Run it only once per server.
This step is still required with an existing acme.sh: the EAB credentials belong to this HTTPCS order.
Separate configuration: --config-home keeps this certificate in its own folder (ACME account, DNS credentials, renewal settings). Keep this option in every following command, it is already added.
mkdir -p /root/.acme.sh-httpcs-mydomain.tld && touch /root/.acme.sh-httpcs-mydomain.tld/account.conf
acme.sh --register-account \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
--server SERVER_URL \
--eab-kid YOUR_KID \
--eab-hmac-key YOUR_HMAC \
-m admin@mydomain.tld
Expected result: Registered then ACCOUNT_THUMBPRINT=….
Already registered instead of Registered
An account already exists for this URL in this configuration: the new EAB credentials are not used and the certificate request will be rejected. Choose Already installed then Separate configuration in the selector and register again. If you are just running the registration again for this same certificate, the message is expected.
externalAccountRequired
The server did not receive the EAB credentials: a line was broken. Each \ must be the last character of its line, with no space after it. Copy the block again with the Copy button.
unauthorized or malformed error mentioning EAB
kid and hmac swapped, truncated or taken from another certificate. Copy them again from the dashboard, with no leading or trailing space.
To prove control of the domain, acme.sh places a file in the .well-known/acme-challenge folder of the website. This test reproduces the operation before the real request. For a multi-domain certificate, repeat the curl line with each domain.
mkdir -p /var/www/html/.well-known/acme-challenge
echo ok > /var/www/html/.well-known/acme-challenge/httpcs-test
curl -i http://mydomain.tld/.well-known/acme-challenge/httpcs-test
rm /var/www/html/.well-known/acme-challenge/httpcs-test
Expected result: HTTP/1.1 200 OK, then ok at the end of the response.
404 Not Found response
This folder is not the one served for this domain. Find the right one with grep -R "root" /etc/nginx/sites-enabled/ (Nginx) or grep -R "DocumentRoot" /etc/apache2/sites-enabled/ (Apache).
Connection refused or timeout
Port 80 is closed (server or hosting firewall, for example ufw allow 80/tcp) or the domain does not point to this server: compare dig +short A mydomain.tld with the server IP.
dig +short AAAA mydomain.tld) while the website does not answer over IPv6. The certificate authority can connect over IPv6: fix or remove this record.
/.well-known/acme-challenge/ path from that redirect.
acme.sh creates the validation TXT record itself through your DNS provider API. Export the credentials in the session: acme.sh saves them in /root/.acme.sh/account.conf during the first request and reuses them at every renewal. If possible, restrict the token to editing this zone.
acme.sh already installed: if these variables are already listed in the “Review” step for the same account, no need to export them again, acme.sh reads them back. Credentials saved for another provider stay untouched.
export CF_Token="TO_FILL_IN"
export CF_Zone_ID="TO_FILL_IN"
export OVH_END_POINT="TO_FILL_IN"
export OVH_AK="TO_FILL_IN"
export OVH_AS="TO_FILL_IN"
export OVH_CK="TO_FILL_IN"
export GANDI_LIVEDNS_TOKEN="TO_FILL_IN"
export INFOMANIAK_API_TOKEN="TO_FILL_IN"
export IONOS_PREFIX="TO_FILL_IN"
export IONOS_SECRET="TO_FILL_IN"
export SCALEWAY_API_TOKEN="TO_FILL_IN"
export AWS_ACCESS_KEY_ID="TO_FILL_IN"
export AWS_SECRET_ACCESS_KEY="TO_FILL_IN"
Export the variables listed for your provider in the acme.sh documentation, as export NAME="value".
Check that the zone is managed by this provider: the name servers shown must belong to it.
dig +short NS mydomain.tld
Error add txt for domain
The API rejected the record: wrong credentials or token without write access to the zone. For OVHcloud, create the token on eu.api.ovh.com/createToken with GET, POST, PUT and DELETE rights on /domain/zone/*.
invalid domain or zone not found
acme.sh cannot find the zone among the domains of the account linked to these credentials. If dig +short NS does show the chosen provider, the zone belongs to another account at this provider (OVHcloud: another customer ID) or the token is not allowed to read it. Create the token again while logged in to the account that manages the zone. Run the command again with --debug 2 to see the API response: This service does not exist means another account, This call has not been granted a missing right. At OVHcloud, a right on /domain/zone/mydomain.tld/* does not cover /domain/zone/mydomain.tld: add this path in GET as well.
acme.sh requests the certificate from the HTTPCS ACME server, performs the chosen validation and downloads the certificate to /root/.acme.sh/.
Keep the single quotes around '*.mydomain.tld': without them, the shell replaces * with the files of the current folder.
acme.sh --issue \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
--server SERVER_URL \
-d mydomain.tld \
-w /var/www/html
acme.sh --issue \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
--server SERVER_URL \
-d mydomain.tld \
--dns dns_xxx
acme.sh --issue \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
--server SERVER_URL \
-d mydomain.tld \
--dns \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
Expected result: Cert success., followed by the certificate and key paths.
Expected result: acme.sh stops on purpose and prints, for each name, a Domain: '_acme-challenge.mydomain.tld' line and a TXT value: '…' line. Keep these values for the next step.
Organization certificate: the domain was already checked during the organization validation. acme.sh may then print Already verified, skip and go straight to issuance. This is expected.
rejectedIdentifier, unauthorized or order rejected
A requested name is not part of your HTTPCS order. Use exactly the ordered domains; for a wildcard, the bare domain and *.domain.
externalAccountRequired at this step
The URL given to --server differs from the registration one: acme.sh tries to create a new account without EAB. Use the same URL in both commands.
*.mydomain.tld
A wildcard cannot be validated over HTTP. Choose a DNS validation in the selector.
Verify error with Invalid response and a 404 code
The folder given to -w is not the website folder. Run the test of the previous step again.
Timeout or Connection refused
Port 80 cannot be reached from the Internet or the domain points elsewhere. Run the test of the previous step again.
invalid domain then Error adding TXT record
acme.sh cannot find the zone among the domains of the account linked to these credentials. If dig +short NS does show the chosen provider, the zone belongs to another account at this provider (OVHcloud: another customer ID) or the token is not allowed to read it. Create the token again while logged in to the account that manages the zone. Run the command again with --debug 2 to see the API response: This service does not exist means another account, This call has not been granted a missing right. At OVHcloud, a right on /domain/zone/mydomain.tld/* does not cover /domain/zone/mydomain.tld: add this path in GET as well.
It seems that your ovh key is changed, let’s clear consumer key first.
Normal message the first time these OVHcloud keys are used: acme.sh forgets the previously saved Consumer Key and uses the one you exported.
--dnssleep 300 to wait five minutes before verification.
In your DNS provider interface, create one TXT record for each value printed by acme.sh, on the _acme-challenge name. Most interfaces append the domain automatically: then enter only _acme-challenge, not the full name.
Wildcard: acme.sh prints two values for the same _acme-challenge.mydomain.tld name, one for the bare domain and one for *.mydomain.tld. Create two separate TXT records; do not replace the first one with the second.
Wait for propagation, then check that every value is visible:
dig +short TXT _acme-challenge.mydomain.tld @1.1.1.1
Once every value appears, complete the request:
acme.sh --renew \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
-d mydomain.tld \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
Manual DNS mode does not renew by itself. ACME certificates are short-lived, a few months: you will have to repeat these steps before each expiry (notAfter date of the certificate). To automate, switch to automatic DNS as soon as possible.
Verify error or Invalid status
A value is missing or not propagated yet. The dig command must print every value. Wait for the zone TTL, then run --renew again.
dig returns nothing
The name is probably doubled (_acme-challenge.mydomain.tld.mydomain.tld). Fix the record by entering only _acme-challenge.
--issue command generates new values. Update the TXT records with the latest values printed.
Copy the key and the full certificate (fullchain) to /etc/ssl. acme.sh remembers these paths and the reload command, and reuses them at every renewal. Never point the web server directly to the files in /root/.acme.sh/.
acme.sh --install-cert -d mydomain.tld \
--config-home /root/.acme.sh-httpcs-mydomain.tld \
--key-file /etc/ssl/private/mydomain.tld-acme.key \
--fullchain-file /etc/ssl/certs/mydomain.tld-acme.crt \
--reloadcmd "RELOAD_COMMAND"
Check that both files exist:
ls -l /etc/ssl/private/mydomain.tld-acme.key /etc/ssl/certs/mydomain.tld-acme.crt
Check that acme.sh remembered this installation. Without these settings, a renewal would not copy the files again nor reload the web server.
grep -o '^Le_\(RealKeyPath\|RealFullChainPath\|ReloadCmd\)' /root/.acme.sh/mydomain.tld_ecc/mydomain.tld.conf
Expected result: Le_RealKeyPath, Le_RealFullChainPath and Le_ReloadCmd.
--install-cert command above again: acme.sh saves the paths and the reload command at that point.
No such file or directory on the .conf file
The certificate was issued with an RSA key: its folder has no _ecc suffix. Remove _ecc from the path.
is not an issued domain, skip
The name given to -d must be the first domain of the issuance step: Main_Domain column of acme.sh --list.
Reload error
The reload command failed, often because of an invalid configuration. Fix the configuration in the next step, then run this command again.
Permission denied
Run the command as root (sudo -i).
Open the website file in /etc/nginx/sites-available/ (or /etc/nginx/conf.d/) and update its HTTPS server block. Remove the old ssl_certificate lines of the website, then test and reload Nginx.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name mydomain.tld;
ssl_certificate /etc/ssl/certs/mydomain.tld-acme.crt;
ssl_certificate_key /etc/ssl/private/mydomain.tld-acme.key;
# rest of your existing configuration
}
nginx -t && systemctl reload nginx
Open the website file in /etc/apache2/sites-available/ and update its port 443 VirtualHost block. Enable the SSL module, test, then reload Apache. On Red Hat, Rocky or Alma, the service is called httpd.
<VirtualHost *:443>
ServerName mydomain.tld
ServerAlias *.mydomain.tld
SSLEngine on
SSLCertificateFile /etc/ssl/certs/mydomain.tld-acme.crt
SSLCertificateKeyFile /etc/ssl/private/mydomain.tld-acme.key
# rest of your existing configuration
</VirtualHost>
a2enmod ssl
apachectl configtest && systemctl reload apache2
Give these two files to your software, the key then the full certificate, and reload it:
/etc/ssl/private/mydomain.tld-acme.key
/etc/ssl/certs/mydomain.tld-acme.crt
cannot load certificate when running nginx -t
The file path is wrong. Compare it with the output of the ls -l command of the previous step.
nginx -T | grep -nE "server_name|ssl_certificate".
Invalid command 'SSLEngine'
The SSL module is not enabled: run a2enmod ssl.
apachectl -S.
Query the website the way a browser does:
openssl s_client -connect mydomain.tld:443 -servername mydomain.tld </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Expected result: issuer mentions Sectigo or TrustSign, notAfter gives the expiry date and the X509v3 Subject Alternative Name line lists all your domains.
Wildcard: the list must contain DNS:*.mydomain.tld and DNS:mydomain.tld. To test a subdomain, replace the -servername value with this subdomain.
Then open the website in a browser and click the padlock: the certificate must be valid and issued by Sectigo or TrustSign.
unable to get local issuer certificate or incomplete chain
The server sends the certificate alone instead of the full certificate. Use the file installed with --fullchain-file.
NET::ERR_CERT_COMMON_NAME_INVALID
The visited name is not in the certificate. A wildcard only covers one subdomain level.
openssl shows the new certificate, the server is correct.
acme.sh checks its certificates every day through cron. It renews this one before it expires, installs it again and runs the reload command, with no action from you.
Separate configuration: the existing cron job does not see this folder, and acme.sh --install-cronjob does not add a second line. Add it yourself:
(crontab -l; echo '0 4 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --config-home "/root/.acme.sh-httpcs-mydomain.tld" > /dev/null') | crontab -
Manual DNS: no automatic renewal. Note the date in the Renew column and repeat the DNS steps before that date, or switch to automatic DNS.
crontab -l | grep acme.sh
acme.sh --list --config-home /root/.acme.sh-httpcs-mydomain.tld
Expected result: a cron line containing acme.sh --cron, and your domain in acme.sh --list with the next renewal date (Renew column).
Separate configuration: two acme.sh --cron lines, one of them with --config-home.
Check the duration of the installed certificate:
openssl x509 -noout -dates -in /etc/ssl/certs/mydomain.tld-acme.crt
Expected result: notAfter gives the expiry date. The date in the Renew column of acme.sh --list must come earlier: acme.sh renews well before expiry.
acme.sh --install-cronjob.
acme.sh --cron --debug to read the error. Common causes: revoked DNS API token, port 80 closed since, or expired HTTPCS order (the ACME account is only valid for the duration of the order).
The installation is complete when the website serves the new certificate, the browser accepts it and acme.sh --list shows a renewal date.