This guide walks you through installing Dehydrated — a lightweight, beginner-friendly tool that automatically gets you an SSL certificate for your website. SSL certificates are what make your website show the padlock ???? and use HTTPS, keeping your visitors' data safe.
Think of Dehydrated as a helpful assistant that:
By the end of this guide, your server will have a working SSL certificate stored and ready to use.
| ACME Server: | The service that issues your SSL certificate (like a digital ID office). |
| DNS Challenge (dns-01): | A way to prove domain ownership by adding a TXT record to your DNS. |
| EAB Credentials: | A special username + password pair to log in to the ACME server. |
| Hook Script: | A helper script that automatically adds/removes DNS records via the Cloudflare API. |
| Cloudflare API Token: | Your permission key to let Dehydrated edit your DNS settings. |
| Detail | Value |
|---|---|
| Supported Systems | Linux, macOS, WSL (Windows Subsystem for Linux), FreeBSD — basically any Unix-like OS |
| DNS Provider Used | Cloudflare (works with others too, via custom hook scripts) |
| Validation Method | dns-01 — proves domain ownership via a DNS TXT record |
| ACME Server | https://acme.https.in/acme/directory |
| Why Dehydrated? | It is a simple bash script with minimal dependencies — easy to understand and customise |
Make sure you have the following items ready before starting. Think of this as your packing checklist before a trip.
Open a terminal and run the command for your operating system:
Ubuntu or Debian Linux
sudo apt-get update
sudo apt-get install -y curl openssl jq gitCentOS or RHEL Linux
sudo yum install -y curl openssl jq gitmacOS
brew install curl openssl jq gitAfter running the command, you should see a success message. If any errors appear, resolve them before continuing.
There are three ways to install Dehydrated. Choose the one that best fits your setup.
This is the safest and most common approach. It downloads the full Dehydrated repository to your server.
Step 1 — Go to the /opt directory and download Dehydrated:
cd /opt
sudo git clone https://github.com/dehydrated-io/dehydrated.git
cd dehydratedStep 2 — Make the script runnable:
sudo chmod +x dehydratedStep 3 — Create a shortcut so you can run 'dehydrated' from anywhere on your system:
sudo ln -s /opt/dehydrated/dehydrated /usr/local/bin/dehydratedStep 4 — Confirm the installation worked:
dehydrated --version'v0.7.x'. If you see 'command not found', check that Step 3 ran without errors.
Use this option if you do not have sudo or root access on the server.
cd ~
git clone https://github.com/dehydrated-io/dehydrated.git
cd dehydrated
chmod +x dehydrated
# Add to PATH so you can use it from any folder
export PATH="$HOME/dehydrated:$PATH"
# Verify installation
./dehydrated --versionTo make this permanent (so it works after you log out and back in), add the export line to your ~/.bashrc file:
echo 'export PATH="$HOME/dehydrated:$PATH"' >> ~/.bashrc
source ~/.bashrcThis downloads just the single script file — useful for quick setups or testing.
sudo curl -L https://raw.githubusercontent.com/dehydrated-io/dehydrated/master/dehydrated \
-o /usr/local/bin/dehydrated
sudo chmod +x /usr/local/bin/dehydrated
dehydrated --versionDehydrated stores all its files (certificates, keys, settings) in /etc/dehydrated. Let's create these folders now.
Run all of these commands one after another:
# Create the main Dehydrated directory
sudo mkdir -p /etc/dehydrated
# Create sub-folders
sudo mkdir -p /etc/dehydrated/accounts
sudo mkdir -p /etc/dehydrated/certs
sudo mkdir -p /etc/dehydrated/hooks
sudo mkdir -p /etc/dehydrated/chains
# Create a log directory
sudo mkdir -p /var/log/dehydrated
# Set correct permissions (accounts and certs are private)
sudo chmod 755 /etc/dehydrated
sudo chmod 700 /etc/dehydrated/accounts
sudo chmod 700 /etc/dehydrated/certsAfter this, your folder layout will look like this:
/etc/dehydrated/
??? accounts/ ? Stores your account keys (keep private!)
??? certs/ ? Where your SSL certificates will be saved
? ??? example.com/
? ??? cert.pem ? Your certificate
? ??? chain.pem ? Intermediate certificate
? ??? fullchain.pem ? cert + chain (use this for web servers)
? ??? privkey.pem ? Your private key (keep secret!)
??? chains/ ? CA certificate chains
??? config ? Main settings file
??? domains.txt ? List of domains you want certificates for
??? hooks/
??? cloudflare.sh ? DNS automation hook scriptThe configuration file tells Dehydrated which ACME server to use, where to store files, and how to handle certificates.
Open a new file using the nano text editor:
sudo nano /etc/dehydrated/configCopy and paste the full configuration below into the file. Read the comments (lines starting with #) to understand what each setting does:
# ?????????????????????????????????????????????????????
# Dehydrated Configuration — Custom ACME Server
# ?????????????????????????????????????????????????????
# ACME SERVER — The service that issues your certificate
CA="https://acme.https.in/acme/directory"
# EAB CREDENTIALS — Uncomment and fill in your credentials
#EAB_KEY_IDENTIFIER="d5a5ac044190a0388e633d492429e789"
#EAB_HMAC_KEY="In73lxoiv_WuLhIlgSznyynM8oBNrkI641IYVmEEOg9"
# Your email — the ACME server will contact you here
CONTACT_EMAIL="your-email@yourdomain.com"
# ?????????????????????????????????????????????????????
# DIRECTORY PATHS
# ?????????????????????????????????????????????????????
BASEDIR="/etc/dehydrated"
ACCOUNTDIR="${BASEDIR}/accounts"
CERTDIR="${BASEDIR}/certs"
CHAINSDIR="${BASEDIR}/chains"
LOCKFILE="${BASEDIR}/lock"
# ?????????????????????????????????????????????????????
# CHALLENGE TYPE — How domain ownership is proved
# ?????????????????????????????????????????????????????
CHALLENGETYPE="dns-01"
HOOK="${BASEDIR}/hooks/cloudflare.sh"
HOOK_CHAIN="yes"
# ?????????????????????????????????????????????????????
# CERTIFICATE SETTINGS
# ?????????????????????????????????????????????????????
KEY_ALGO="rsa"
KEYSIZE="2048"
OCSP_MUST_STAPLE="no"
# ?????????????????????????????????????????????????????
# RENEWAL SETTINGS
# ?????????????????????????????????????????????????????
RENEW_DAYS="30" # Renew 30 days before expiry
PRIVATE_KEY_RENEW="yes"
# ?????????????????????????????????????????????????????
# ADVANCED SETTINGS (safe to leave as defaults)
# ?????????????????????????????????????????????????????
MINIMUM_VALID_DAYS="30"
AUTO_CLEANUP="yes"
IP_VERSION=""
PRIVATE_KEY_ROLLOVER="no"
OPENSSL_CNF=""
CURL_OPTS="--connect-timeout 60 --max-time 600"
# Optional: Enable logging to a file
# LOGFILE="/var/log/dehydrated/dehydrated.log"Save and close the file: press Ctrl+X, then Y, then Enter.
Now lock down the file so only root can read it:
sudo chmod 600 /etc/dehydrated/configEAB credentials act like a username and password for your ACME account. You get them from your ACME server administrator. There are three ways to provide them to Dehydrated — choose one:
Open your config file and find the EAB section. Remove the # symbols in front of the two EAB lines and fill in your actual credentials:
sudo nano /etc/dehydrated/config
# Find these lines and remove the # from the front:
EAB_KEY_IDENTIFIER="paste-your-key-id-here"
EAB_HMAC_KEY="paste-your-hmac-key-here"If you prefer not to save credentials in a file, you can set them as temporary environment variables in your terminal session:
export EAB_KEY_IDENTIFIER="paste-your-key-id-here"
export EAB_HMAC_KEY="paste-your-hmac-key-here"Note: These will be lost when you close the terminal session.
You can also pass the credentials directly when you run the register command:
dehydrated --register --accept-terms \
--eab-kid "paste-your-key-id-here" \
--eab-hmac-key "paste-your-hmac-key-here"This is the script that automatically adds and removes the DNS TXT record needed to prove you own your domain. When Dehydrated needs to verify your domain, it calls this script, which talks to the Cloudflare API on your behalf.
Open a new file:
sudo nano /etc/dehydrated/hooks/cloudflare.shPaste in the entire hook script below:
#!/usr/bin/env bash
# Cloudflare DNS Hook for Dehydrated
# This script is called automatically by Dehydrated during certificate issuance.
# It creates and deletes DNS TXT records using the Cloudflare API.
set -e
set -u
set -o pipefail
# ?? Configuration ??????????????????????????????????
CF_API_TOKEN="${CF_API_TOKEN:-}"
CF_ACCOUNT_ID="${CF_ACCOUNT_ID:-}"
CF_API_URL="https://api.cloudflare.com/client/v4"
# ?? Logging ????????????????????????????????????????
LOG_FILE="${LOG_FILE:-/var/log/dehydrated/cloudflare-hook.log}"
mkdir -p "$(dirname "$LOG_FILE")"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" | tee -a "$LOG_FILE" >&2
}
# ?? Get Zone ID from Cloudflare ????????????????????
get_zone_id() {
local domain="$1"
local root_domain
root_domain=$(echo "$domain" | awk -F. '{print $(NF-1)"."$NF}')
local response
response=$(curl -s -X GET "${CF_API_URL}/zones?name=${root_domain}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
local zone_id
zone_id=$(echo "$response" | jq -r '.result[0].id // empty')
if [[ -z "$zone_id" ]]; then
error "Failed to get zone ID for ${root_domain}"
return 1
fi
echo "$zone_id"
}
# ?? Create TXT Record ??????????????????????????????
create_txt_record() {
local domain="$1"
local token="$2"
local zone_id
log "Creating TXT record for _acme-challenge.${domain}"
zone_id=$(get_zone_id "$domain")
local response
response=$(curl -s -X POST "${CF_API_URL}/zones/${zone_id}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"TXT\",\"name\":\"_acme-challenge.${domain}\",\"content\":\"${token}\",\"ttl\":120}")
local record_id
record_id=$(echo "$response" | jq -r '.result.id // empty')
if [[ -z "$record_id" ]]; then
error "Failed to create TXT record"
return 1
fi
log "TXT record created (ID: ${record_id})"
echo "$record_id"
}
# ?? Delete TXT Record ??????????????????????????????
delete_txt_record() {
local domain="$1"
local zone_id
log "Deleting TXT record for _acme-challenge.${domain}"
zone_id=$(get_zone_id "$domain")
local response
response=$(curl -s -X GET "${CF_API_URL}/zones/${zone_id}/dns_records?type=TXT&name=_acme-challenge.${domain}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
local record_ids
record_ids=$(echo "$response" | jq -r '.result[].id')
for record_id in $record_ids; do
curl -s -X DELETE "${CF_API_URL}/zones/${zone_id}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" > /dev/null
log "TXT record deleted (ID: ${record_id})"
done
}
# ?? Wait for DNS to Propagate ??????????????????????
wait_for_propagation() {
local domain="$1"
local token="$2"
local max_attempts=30
local attempt=0
log "Waiting for DNS propagation for _acme-challenge.${domain}"
while [[ $attempt -lt $max_attempts ]]; do
if dig +short TXT "_acme-challenge.${domain}" @1.1.1.1 | grep -q "$token"; then
log "DNS propagation confirmed (attempt $((attempt + 1)))"
return 0
fi
attempt=$((attempt + 1))
log "Not propagated yet (attempt $((attempt))/${max_attempts})"
sleep 10
done
error "DNS propagation timeout"
return 1
}
# ?? Main Hook Handler ??????????????????????????????
main() {
local handler="$1"
shift
case "$handler" in
deploy_challenge)
local domain="$1" token_filename="$2" token_value="$3"
if [[ -z "$CF_API_TOKEN" ]]; then error "CF_API_TOKEN not set"; exit 1; fi
record_id=$(create_txt_record "$domain" "$token_value")
echo "$record_id" > "/tmp/dehydrated_cf_${domain}.txt"
wait_for_propagation "$domain" "$token_value"
;;
clean_challenge)
local domain="$1"
delete_txt_record "$domain"
rm -f "/tmp/dehydrated_cf_${domain}.txt"
;;
deploy_cert)
local domain="$1" keyfile="$2" certfile="$3" fullchainfile="$4" chainfile="$5"
log "Certificate deployed: ${certfile}"
;;
unchanged_cert)
log "Certificate unchanged, no renewal needed"
;;
startup_hook) log 'Dehydrated starting...' ;;
exit_hook) log 'Dehydrated finished' ;;
*) error "Unknown hook: ${handler}" ;;
esac
}
main "$@"Save and close: Ctrl+X ? Y ? Enter.
The script won't run unless you give it execute permission:
sudo chmod +x /etc/dehydrated/hooks/cloudflare.shVerify it has the right permissions:
ls -la /etc/dehydrated/hooks/cloudflare.sh
# You should see: -rwxr-xr-x (the 'x' means executable)Your API token is what lets the hook script talk to Cloudflare. You need to create a scoped token (one with only the permissions it needs — this is safer than using your global API key).
Create a file to store your Cloudflare credentials:
sudo nano /etc/dehydrated/cloudflare.envAdd the following, replacing the placeholder values with your actual credentials:
# Cloudflare API Credentials
export CF_API_TOKEN="paste-your-cloudflare-api-token-here"
export CF_ACCOUNT_ID="paste-your-cloudflare-account-id-here"
# Optional: specify a log file for the hook script
export LOG_FILE="/var/log/dehydrated/cloudflare-hook.log"Save and close, then lock down the file:
sudo chmod 600 /etc/dehydrated/cloudflare.envLet's make sure Cloudflare accepts your token before going further:
source /etc/dehydrated/cloudflare.env
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json""status": "active". If you see an authentication error, double-check that you copied the token correctly.
Now it's time to register your account on the ACME server. This is a one-time step — you only need to do this once per server.
source /etc/dehydrated/config
source /etc/dehydrated/cloudflare.envRun the following command. It reads your EAB credentials from the config file and accepts the ACME server's terms of service:
dehydrated --register --accept-termsIf your EAB credentials are not in the config file, you can pass them directly:
dehydrated --register --accept-terms \
--eab-kid "your-eab-key-id-here" \
--eab-hmac-key "your-hmac-key-here"Check that account files were created:
ls -la /etc/dehydrated/accounts//etc/dehydrated/accounts/. If the folder is empty, registration did not complete — re-read the error output and check your EAB credentials.
Dehydrated reads a simple text file called domains.txt to know which domains to get certificates for. Each line in the file becomes one certificate.
sudo nano /etc/dehydrated/domains.txtSingle Domain
yourdomain.comDomain with Aliases — One Certificate Covers Multiple Names
example.com www.example.com mail.example.comMultiple Separate Certificates — Each Domain Gets Its Own
example.com www.example.com
api.example.com
mail.example.comWildcard Certificate — Covers All Subdomains
*.yourdomain.com yourdomain.comOrganised with Comments
# Production website
example.com www.example.com
# API endpoint
api.example.com
# Staging environment
*.staging.example.com staging.example.comThis is the big moment! Dehydrated will now contact the ACME server, prove you own your domain via DNS, and collect your certificate.
source /etc/dehydrated/cloudflare.envThe --cron flag tells Dehydrated to check all domains in domains.txt and issue/renew as needed:
dehydrated --cronIf you want to see detailed output of what's happening (recommended for first run):
dehydrated --cron --force --verboseIssue Certificate for a Specific Domain Only
dehydrated --cron --domain yourdomain.comDry Run — Test Without Actually Issuing
dehydrated --cron --dry-runForce Re-issue Even if Certificate is Still Valid
dehydrated --cron --forceAfter a successful run, Dehydrated saves all certificate files in /etc/dehydrated/certs/[your-domain]/
/etc/dehydrated/certs/yourdomain.com/
??? cert.csr ? Certificate Signing Request (created during issuance)
??? cert.pem ? Your certificate (domain certificate only)
??? chain.pem ? Intermediate certificate from the CA
??? fullchain.pem ? cert.pem + chain.pem combined ? USE THIS for web servers
??? privkey.pem ? Your private key ? KEEP THIS SECRET!
??? privkey.pem.old ? Previous key (only exists if key was rotated)| File | What It Is & When to Use It |
|---|---|
| fullchain.pem | Use this with your web server (Nginx, Apache). It includes your certificate AND the intermediate chain. |
| cert.pem | Just your certificate. Rarely needed on its own. |
| privkey.pem | Your private key. Treat this like a password — never share it! |
| chain.pem | The intermediate certificate(s) only. Some tools may need this separately. |
Let's make sure everything worked correctly.
Check Certificate Details
openssl x509 -in /etc/dehydrated/certs/yourdomain.com/cert.pem -noout -textCheck Certificate Expiry Dates
openssl x509 -in /etc/dehydrated/certs/yourdomain.com/cert.pem -noout -dates
# You should see: notBefore=... notAfter=...Verify the Certificate Chain is Complete
openssl verify -CAfile /etc/dehydrated/certs/yourdomain.com/chain.pem \
/etc/dehydrated/certs/yourdomain.com/cert.pemTest the Live Certificate on Your Server
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -datesIf you want to automate the entire installation and first certificate issuance, you can use this all-in-one script. Edit the variables at the top to match your setup, then run it.
#!/bin/bash
# ?????????????????????????????????????????????????????????
# Dehydrated Quick Start Script
# Edit the variables below, then run: sudo bash quickstart.sh
# ?????????????????????????????????????????????????????????
set -e
# ?? YOUR SETTINGS ??????????????????????????????????????
DOMAIN="yourdomain.com"
ACME_SERVER="https://acme.https.in/acme/directory"
EAB_KEY="paste-your-eab-key-id-here"
HMAC_KEY="paste-your-hmac-key-here"
EMAIL="your-email@yourdomain.com"
CF_TOKEN="paste-your-cloudflare-token-here"
# ???????????????????????????????????????????????????????
export CF_API_TOKEN="$CF_TOKEN"
# Create all necessary directories
mkdir -p /etc/dehydrated/{accounts,certs,hooks,chains}
mkdir -p /var/log/dehydrated
# Set up domains.txt
echo "$DOMAIN" > /etc/dehydrated/domains.txt
# Register account with ACME server (skip if already registered)
if [[ ! -d "/etc/dehydrated/accounts" ]] || [[ -z "$(ls -A /etc/dehydrated/accounts)" ]]; then
echo "Registering account..."
dehydrated --register --accept-terms \
--eab-kid "$EAB_KEY" \
--eab-hmac-key "$HMAC_KEY"
fi
# Issue certificate
echo "Issuing certificate for $DOMAIN..."
dehydrated --cron --force --verbose
echo "Done! Certificate saved to: /etc/dehydrated/certs/${DOMAIN}/"Save this script, make it executable, and run it:
chmod +x quickstart.sh
sudo bash quickstart.shIf something goes wrong, here are the most common issues and how to fix them.
| Problem | Cause | Fix |
|---|---|---|
| dehydrated: command not found | Script not in PATH or symlink not created | Run: export PATH="/usr/local/bin:$PATH" or re-do the symlink step |
| Hook script not running | Missing execute permission on the script file | Run: chmod +x /etc/dehydrated/hooks/cloudflare.sh |
| Cloudflare API error / 403 Forbidden | Incorrect API token or wrong permissions | Re-verify token with the curl test command and check Zone permissions |
| EAB registration failed | Wrong EAB key ID or HMAC key, or wrong ACME server URL | Re-run with --verbose flag. Contact your ACME admin. |
| DNS propagation timeout | Cloudflare updated the record but global DNS is slow | Increase the sleep value in wait_for_propagation() in cloudflare.sh |
Congratulations — your SSL certificate is now set up! Here's what to do next to complete your setup:
/etc/dehydrated/certs/yourdomain.com/fullchain.pem and privkey.pem.dehydrated --cron once per day.deploy_cert hook in cloudflare.sh to automatically restart your web server after a certificate is renewed.dehydrated --cron --dry-run to make sure automatic renewal will work correctly.Use this checklist to make sure you haven't missed any steps:
| Done |
|
|---|