79827895

Date: 2025-11-23 13:37:47
Score: 1
Natty:
Report link

for good or ill, @user1072814 inspired me to give it another go, and with a little help form chatgpt (it gets SO much wrong that im not worried about our AI overlords taking over just yet) this is what i ended up with, works on all my devices by using the endpoint given at at end of script

good enough for my meagre needs (ad blocking), but shared here in case anyone benefits - change variables at top of script to suit. This was run and tested on an Oracle free tier Ubuntu minimal setup from bare, remember to check and open any ports you may require in the Oracle pages (as long as you have 80, 443 allowed, youre golden). Edit the toml block for dnscrypt-prozy below

# dnscrypt-proxy config - for server-side DoH TLS

as you see fit for your own upstream server (default is cloudflare, again im only using this for centralised adblocking and not tin foil hat relays and anonymising) and personal dnscrypt options

#!/usr/bin/env bash
# dnscrypt_oneclick_final_doh_direct_b.sh
# One-click installer: dnscrypt-proxy (DoH TLS on 443) + nginx (HTTP only) +
# Let's Encrypt (ECDSA secp256r1) + renewal hook + health monitor + alerts
#
DOMAIN="your domain here"
EMAIL="your email here"

set -euo pipefail
set -x

CERT_DIR="/etc/letsencrypt/live/${DOMAIN}"
WEBROOT="/var/www/html"
WWW_DIR="/var/www/ca"
DNSCRYPT_CONF="/etc/dnscrypt-proxy/dnscrypt-proxy.toml"
DNSCRYPT_USER_FILES="/usr/local/dnscrypt-proxy"
DOH_PORT=443
LOCAL_DOH_PORT=3000
NGINX_HTTP_CONF="/etc/nginx/sites-available/dnscrypt-http.conf"
NGINX_HTTP_ENABLED="/etc/nginx/sites-enabled/dnscrypt-http.conf"
RENEW_HOOK_DIR="/etc/letsencrypt/renewal-hooks/deploy"
HEALTH_SCRIPT="/usr/local/bin/dnscrypt_health.sh"
LOG_FILE="/var/log/dnscrypt_health.log"

if [ "$(id -u)" -ne 0 ]; then
  echo "Run as root: sudo $0"
  exit 1
fi

export DEBIAN_FRONTEND=noninteractive

echo "=== Installing packages ==="
apt update
apt install -y dnscrypt-proxy nginx openssl curl unzip iptables-persistent netfilter-persistent certbot python3-certbot-nginx mailutils cron

echo "=== Creating webroot & WWW dirs ==="
mkdir -p "${WEBROOT}/.well-known/acme-challenge" "${WWW_DIR}"
chown -R www-data:www-data "${WEBROOT}" "${WWW_DIR}"

# -------------------------
# iptables (safe insert)
# -------------------------
insert_if_missing() {
  if iptables -C "$@" 2>/dev/null; then
    echo "Rule exists: $*"
  else
    iptables -I "$@"
    echo "Inserted: $*"
  fi
}
if ! iptables -C INPUT -j ACCEPT 2>/dev/null; then
  iptables -I INPUT -j ACCEPT
fi
insert_if_missing INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT || true
insert_if_missing INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT || true
insert_if_missing INPUT 6 -m state --state NEW -p udp --dport 443 -j ACCEPT || true
insert_if_missing INPUT -m state --state NEW -p tcp --dport 22 -j ACCEPT || true
netfilter-persistent save || iptables-save > /etc/iptables/rules.v4

# -------------------------
# dnscrypt-proxy config (minimal + DoH on 0.0.0.0:443)
# -------------------------
echo "=== Backing up and writing dnscrypt-proxy config ==="
[ -f "${DNSCRYPT_CONF}" ] && cp "${DNSCRYPT_CONF}" "${DNSCRYPT_CONF}.bak-$(date +%s)" || true

cat > "${DNSCRYPT_CONF}" <<'TOML'
# dnscrypt-proxy config - for server-side DoH TLS

# local DNS listeners (for local clients and system)
server_names = ['cloudflare']
listen_addresses = ['127.0.0.1:53']

ipv4_servers = true
ipv6_servers = false
dnscrypt_servers = true
doh_servers = true
require_dnssec = true
require_nolog = true
require_nofilter = true

log_file = '/var/log/dnscrypt-proxy/dnscrypt-proxy.log'
log_level = 2
log_file_latest = true

block_ipv6 = true
block_unqualified = true
block_undelegated = true
reject_ttl = 10

cache = true
cache_size = 4096
cache_min_ttl = 2400
cache_max_ttl = 86400
cache_neg_min_ttl = 60
cache_neg_max_ttl = 600

netprobe_address = '1.1.1.1:53'

# External DoH/TLS listener (dnscrypt-proxy will terminate TLS directly on 0.0.0.0:443)
[local_doh]
# listen on all interfaces port 443 for external DoH clients
listen_addresses = ['0.0.0.0:443']
path = '/dns-query'
# cert paths will be populated by installer (letsencrypt files)
cert_file = '/etc/letsencrypt/live/amdnscrypt.ddns.net/fullchain.pem'
cert_key_file = '/etc/letsencrypt/live/amdnscrypt.ddns.net/privkey.pem'

# Optional: also serve a local DoH endpoint (unused by external clients,
# but useful for testing or nginx reverse-proxy if you want)
# Add another local_doh listener if your dnscrypt-proxy supports it (some versions vary)
# local_doh_listen = ['127.0.0.1:3000']

[captive_portals]
map_file = '/usr/local/dnscrypt-proxy/captive-portals.txt'

[blocked_names]
blocked_names_file = '/usr/local/dnscrypt-proxy/blocked-names.txt'
log_file = '/usr/local/dnscrypt-proxy/blocked-names.log'

[blocked_ips]
blocked_ips_file = '/usr/local/dnscrypt-proxy/blocked-ips.txt'
log_file = '/usr/local/dnscrypt-proxy/blocked-ips.log'

[allowed_names]
allowed_names_file = '/usr/local/dnscrypt-proxy/allowed-names.txt'

[allowed_ips]
allowed_ips_file = '/usr/local/dnscrypt-proxy/allowed-ips.txt'


[broken_implementations]
fragments_blocked = [
  'cisco',
  'cisco-ipv6',
  'cisco-familyshield',
  'cisco-familyshield-ipv6',
  'cisco-sandbox',
  'cleanbrowsing-adult',
  'cleanbrowsing-adult-ipv6',
  'cleanbrowsing-family',
  'cleanbrowsing-family-ipv6',
  'cleanbrowsing-security',
  'cleanbrowsing-security-ipv6',
]

[sources]
  [sources.public-resolvers]
  urls = [
    'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md',
    'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'
  ]
  cache_file = 'public-resolvers.md'
  minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
  refresh_delay = 73
  prefix = ''
TOML

# stop dnscrypt-proxy so it doesn't try to bind before certs exist
systemctl daemon-reload
systemctl enable dnscrypt-proxy || true
systemctl stop dnscrypt-proxy || true

# -------------------------
# Ensure nginx will not fail loading SSL during bootstrap
# -------------------------
echo "=== moving existing enabled sites aside and disabling SSL confs ==="
mkdir -p /etc/nginx/sites-enabled.bak
if [ -d /etc/nginx/sites-enabled ]; then
  for s in /etc/nginx/sites-enabled/*; do
    [ -e "$s" ] || continue
    mv -f "$s" /etc/nginx/sites-enabled.bak/ || true
  done
fi
if [ -d /etc/nginx/conf.d ]; then
  for f in /etc/nginx/conf.d/*.conf; do
    [ -f "$f" ] || continue
    if grep -qi "ssl_certificate" "$f"; then
      mv -f "$f" "${f}.disabled-ssl" || true
    fi
  done
fi
if grep -qi "ssl_certificate" /etc/nginx/nginx.conf 2>/dev/null; then
  cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%s)
  sed -i '/ssl_certificate/d' /etc/nginx/nginx.conf || true
  sed -i '/ssl_certificate_key/d' /etc/nginx/nginx.conf || true
fi

# -------------------------
# Temporary HTTP-only nginx config for ACME
# -------------------------
echo "=== installing temporary HTTP-only nginx config ==="
cat > "${NGINX_HTTP_CONF}" <<NGHTTP
server {
    listen 80;
    listen [::]:80;
    server_name ${DOMAIN};

    root ${WWW_DIR};
    index index.html;

    location /.well-known/acme-challenge/ {
        root ${WEBROOT};
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}
NGHTTP

ln -sf "${NGINX_HTTP_CONF}" "${NGINX_HTTP_ENABLED}"
nginx -t
systemctl restart nginx

# -------------------------
# Obtain ECDSA cert with certbot (webroot)
# -------------------------
echo "=== Requesting ECDSA certificate from Let's Encrypt (secp256r1) ==="
certbot certonly --webroot -w "${WEBROOT}" -d "${DOMAIN}" --non-interactive --agree-tos -m "${EMAIL}" --key-type ecdsa --elliptic-curve secp256r1 || {
  echo "Certbot issuance failed; inspect /var/log/letsencrypt/letsencrypt.log"
  exit 1
}

if [ ! -f "${CERT_DIR}/fullchain.pem" ]; then
  echo "Expected certs not found in ${CERT_DIR}; aborting"
  exit 1
fi

# fix permissions so dnscrypt-proxy and nginx can read certificate files
chown -R root:root /etc/letsencrypt
chmod 644 "${CERT_DIR}/fullchain.pem" || true
chmod 640 "${CERT_DIR}/privkey.pem" || true
chown root:www-data "${CERT_DIR}/privkey.pem" || true

# -------------------------
# Ensure systemd socket not masked and start dnscrypt-proxy
# -------------------------
echo "=== ensuring dnscrypt-proxy socket is unmasked and starting service ==="
# If systemd socket unit exists and is masked, unmask it
if systemctl list-unit-files | grep -q '^dnscrypt-proxy.socket'; then
  sudo systemctl unmask dnscrypt-proxy.socket || true
  sudo systemctl enable dnscrypt-proxy.socket || true
  sudo systemctl start dnscrypt-proxy.socket || true
fi

# start the dnscrypt-proxy service which will bind 0.0.0.0:443
systemctl restart dnscrypt-proxy || {
  # If systemd refuses because socket is masked, try to start service directly after unmasking
  systemctl daemon-reload
  systemctl unmask dnscrypt-proxy.socket || true
  systemctl restart dnscrypt-proxy || true
}
sleep 1
systemctl status dnscrypt-proxy --no-pager || true

# -------------------------
# Finalize nginx (keep HTTP-only)
# -------------------------
echo "=== finalizing nginx (HTTP-only, ACME/static only) ==="
# remove temporary enabled file (site still available in sites-available)
rm -f "${NGINX_HTTP_ENABLED}"
# restore other non-SSL enabled sites from backup if they exist (they were moved aside earlier)
if [ -d /etc/nginx/sites-enabled.bak ]; then
  for f in /etc/nginx/sites-enabled.bak/*; do
    [ -e "$f" ] || continue
    mv -f "$f" /etc/nginx/sites-enabled/ || true
  done
fi
nginx -t
systemctl reload nginx

# -------------------------
# Install certbot renewal hook to reload nginx and restart dnscrypt-proxy
# -------------------------
mkdir -p "${RENEW_HOOK_DIR}"
cat > "${RENEW_HOOK_DIR}/reload-dnscrypt-nginx.sh" <<'EOF'
#!/usr/bin/env bash
LOG="/var/log/letsencrypt-renewal-reload.log"
{
  echo "[$(date)] deploy hook started: RENEWED_LINEAGE=${RENEWED_LINEAGE}"
  systemctl reload nginx || systemctl restart nginx || true
  systemctl restart dnscrypt-proxy || true
  echo "[$(date)] deploy hook finished"
} >> "$LOG" 2>&1
EOF
chmod +x "${RENEW_HOOK_DIR}/reload-dnscrypt-nginx.sh"

# -------------------------
# Health monitor (self-heal + email) - installed
# -------------------------
cat > "${HEALTH_SCRIPT}" <<'HSH'
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="'"${DOMAIN}"'"
EMAIL="'"${EMAIL}"'"
CERT="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"
LOG="/var/log/dnscrypt_health.log"
TMPDIR="/var/tmp/dnscrypt_health"
mkdir -p "${TMPDIR}"
touch "${LOG}"
STATE_CERT="${TMPDIR}/cert_alert_sent"
STATE_NGINX="${TMPDIR}/nginx_alert_sent"
STATE_DC="${TMPDIR}/dnscrypt_alert_sent"

send_mail() {
  local subject="$1"
  local body="$2"
  echo -e "${body}" | mail -s "${subject}" "${EMAIL}"
  echo "[$(date)] Sent alert: ${subject}" >> "${LOG}"
}

DRY_RUN=0
if [ "${1:-}" = "--dry-run" ]; then
  DRY_RUN=1
fi

# Certificate expiry
if [ -f "${CERT}" ]; then
  enddate=$(openssl x509 -in "${CERT}" -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
  if [ -n "${enddate}" ]; then
    endsec=$(date -d "${enddate}" +%s)
    now=$(date +%s)
    days_left=$(( (endsec - now) / 86400 ))
  else
    days_left=0
  fi
else
  days_left=0
fi

if [ "${days_left}" -lt 10 ]; then
  SUBJECT="[ALERT] Certificate for ${DOMAIN} expires in ${days_left} days"
  BODY="Certificate for ${DOMAIN} expires in ${days_left} days.\n\nCheck: sudo openssl x509 -in ${CERT} -noout -text\n\nThis is an automated alert."
  if [ "${DRY_RUN}" -eq 1 ]; then
    echo "DRY RUN: ${SUBJECT}"
    echo -e "${BODY}"
  else
    today=$(date +%F)
    if [ ! -f "${STATE_CERT}" ] || [ "$(cat "${STATE_CERT}")" != "${today}" ]; then
      send_mail "${SUBJECT}" "${BODY}"
      echo "${today}" > "${STATE_CERT}"
    fi
  fi
fi

attempt_restart_and_check() {
  local svc="$1"
  local statefile="$2"
  echo "[$(date)] Attempting restart: ${svc}" >> "${LOG}"
  systemctl restart "${svc}" || true
  sleep 5
  if systemctl is-active --quiet "${svc}"; then
    echo "[$(date)] ${svc} active after restart" >> "${LOG}"
    [ -f "${statefile}" ] && rm -f "${statefile}"
    return 0
  else
    echo "[$(date)] ${svc} still down after restart" >> "${LOG}"
    return 1
  fi
}

# nginx
if ! systemctl is-active --quiet nginx; then
  if [ "${DRY_RUN}" -eq 1 ]; then
    echo "DRY RUN: nginx inactive"
  else
    if ! attempt_restart_and_check "nginx" "${STATE_NGINX}"; then
      SUBJECT="[ALERT] nginx is not running on ${DOMAIN}"
      BODY="nginx is not active on $(hostname) as of $(date). Restart attempts failed.\n\nJournalctl (last 50):\n$(journalctl -u nginx -n 50 --no-pager)\n"
      today=$(date +%F)
      if [ ! -f "${STATE_NGINX}" ] || [ "$(cat "${STATE_NGINX}")" != "${today}" ]; then
        send_mail "${SUBJECT}" "${BODY}"
        echo "${today}" > "${STATE_NGINX}"
      fi
    fi
  fi
fi

# dnscrypt-proxy
if ! systemctl is-active --quiet dnscrypt-proxy; then
  if [ "${DRY_RUN}" -eq 1 ]; then
    echo "DRY RUN: dnscrypt-proxy inactive"
  else
    if ! attempt_restart_and_check "dnscrypt-proxy" "${STATE_DC}"; then
      SUBJECT="[ALERT] dnscrypt-proxy is not running on $(hostname)"
      BODY="dnscrypt-proxy is not active on $(hostname) as of $(date). Restart attempts failed.\n\nJournalctl (last 50):\n$(journalctl -u dnscrypt-proxy -n 50 --no-pager)\n"
      today=$(date +%F)
      if [ ! -f "${STATE_DC}" ] || [ "$(cat "${STATE_DC}")" != "${today}" ]; then
        send_mail "${SUBJECT}" "${BODY}"
        echo "${today}" > "${STATE_DC}"
      fi
    fi
  fi
fi

exit 0
HSH

chmod +x "${HEALTH_SCRIPT}"

# Cron job every 6 hours
cat > /etc/cron.d/dnscrypt_health <<'CRON'
0 */6 * * * root /usr/local/bin/dnscrypt_health.sh >> /var/log/dnscrypt_health.log 2>&1
CRON

touch "${LOG_FILE}"
chown root:root "${LOG_FILE}"
chmod 644 "${LOG_FILE}"

# Dry-run renewal test
certbot renew --dry-run || echo "certbot dry-run failed - check /var/log/letsencrypt/letsencrypt.log"

# Create directory for extra dnscrypt files (blocked-names.txt. allowed-names.txt etc) and populate with basic files live form dnscrypt github - file paths are set in dnscrypt-proxy.toml
if [ ! -d "$DNSCRYPT_USER_FILES" ]; then 
    echo "Creating $DNSCRYPT_USER_FILES for block and allowed lists..."
    mkdir -p "$DNSCRYPT_USER_FILES"
    echo "Downloading basic block and allow lists for domains and ips + captive portal info to  $DNSCRYPT_USER_FILES..."
    curl -o "$DNSCRYPT_USER_FILES/blocked-names.txt" https://raw.githubusercontent.com/DNSCrypt/dnscrypt-proxy/refs/heads/master/dnscrypt-proxy/example-blocked-names.txt
    curl -o "$DNSCRYPT_USER_FILES/blocked-ips.txt" https://raw.githubusercontent.com/DNSCrypt/dnscrypt-proxy/refs/heads/master/dnscrypt-proxy/example-blocked-ips.txt
    curl -o "$DNSCRYPT_USER_FILES/allowed-names.txt" https://raw.githubusercontent.com/DNSCrypt/dnscrypt-proxy/refs/heads/master/dnscrypt-proxy/example-allowed-names.txt
    curl -o "$DNSCRYPT_USER_FILES/allowed-ips.txt" https://raw.githubusercontent.com/DNSCrypt/dnscrypt-proxy/refs/heads/master/dnscrypt-proxy/example-blocked-names.txt
    curl -o "$DNSCRYPT_USER_FILES/captive-portals.txt" https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-captive-portals.txt
fi

# Final client instructions
cat <<'EOF'

INSTALL COMPLETE
Domain: '"${DOMAIN}"'
DoH endpoint: https://${DOMAIN}/dns-query  (dnscrypt-proxy terminates TLS on 443)
Nginx: HTTP-only for ACME & static files (port 80)
Alerts to: ${EMAIL}

Browser instructions:
1) Firefox Desktop:
   Preferences → Settings → Network Settings → Enable DNS over HTTPS → Custom: https://${DOMAIN}/dns-query

2) Firefox Android:
   Settings → General → Network Settings → Use custom DoH: https://${DOMAIN}/dns-query

3) Chrome Desktop:
   Settings → Privacy and security → Security → Use secure DNS → Custom: https://${DOMAIN}/dns-query

4) Chrome Android:
   Settings → Privacy and security → Use secure DNS → Custom provider: https://${DOMAIN}/dns-query

EOF

echo "=== DONE ==="



Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @user1072814
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: Adrian M