[
  {
    "path": ".gitignore",
    "content": ".vscode/\nPRIVATE_NOTES.txt\n.idea/*\n"
  },
  {
    "path": "README.md",
    "content": "# synology-letsencrypt\n\nCreate and manage a [Let's Encrypt](https://letsencrypt.org/) certificate on a Synology NAS.\n\nThis project uses [lego](https://go-acme.github.io/lego/) and the [ACME DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) with any supported [DNS provider](https://go-acme.github.io/lego/dns/).\n\n\n## Install & Update Script\n\nTo **install** or **update** `synology-letsencrypt`, run the [install script](install.sh). You can either download and run the script manually, or use the following curl command:\n\n```sh\ncurl -sSL https://raw.githubusercontent.com/JessThrysoee/synology-letsencrypt/master/install.sh | bash\n```\n\nThe script must be run as root. You can SSH into your NAS as an admin user and then run `sudo -i` to become root (using the same password as the admin user).\n\n> [!IMPORTANT]\n> Migration from `lego` v4 to v5\n\nIf you are updating from a version of `lego` earlier than v5, note that v5 introduces breaking changes to the CLI, directory structure, and JSON file format.\n\nAfter running the [install script](install.sh) to update `lego` and this repository's scripts to the latest versions, run the new v5 command `lego migrate` before running any other commands. For example:\n\n    /usr/local/bin/lego migrate --path /usr/local/etc/synology-letsencrypt/\n\nMore information is available in the [v5 blog post](https://ldez.github.io/blog/2026/05/11/lego-v5/).\n\nAlso note that the optional environment variables `LEGO_RUN_OPTIONS` and `LEGO_RENEW_OPTIONS` in your `env` file have been replaced with a single optional variable, `LEGO_OPTIONS`.\n\n## Configuration\n\nUpdate `/usr/local/etc/synology-letsencrypt/env` with your domain(s), email address, and DNS API key:\n\n```sh\nDOMAINS=(--domains \"example.com\" --domains \"*.example.com\")\nEMAIL=\"user@example.com\"\n\n# Specify the DNS provider (this example is from https://go-acme.github.io/lego/dns/simply/)\nDNS_PROVIDER=\"simply\"\nexport SIMPLY_ACCOUNT_NAME=XXXXXXX\nexport SIMPLY_API_KEY=XXXXXXXXXX\nexport SIMPLY_PROPAGATION_TIMEOUT=1800\nexport SIMPLY_POLLING_INTERVAL=30\n\n# Should you need it; additional options can be passed directly to lego\n#LEGO_OPTIONS=(--key-type \"RSA4096\" --ari-disable --server \"letsencrypt-staging\")\n```\n\nYou should now be able to run `/usr/local/bin/synology-letsencrypt.sh`.\n\nTo schedule a daily task, log in to Synology DSM and add a user-defined script:\n\n    Synology DSM -> Control Panel -> Task Scheduler\n       Create -> Scheduled Task -> User-defined script\n          General -> User = root\n          Task Settings -> User-defined script = /bin/bash /usr/local/bin/synology-letsencrypt.sh\n\nTo secure services with the certificate, see the [Configure Certificates](https://kb.synology.com/en-global/DSM/help/DSM/AdminCenter/connection_certificate?version=7#b_64) documentation.\n\n### Multiple Certificates\n\nIf you need to generate multiple certificates, you can run `synology-letsencrypt.sh` with the path to a certificate-specific configuration:\n\n\n```shellsession\n$ /usr/local/bin/synology-letsencrypt.sh -p /usr/local/etc/synology-letsencrypt/example.com\n$ /usr/local/bin/synology-letsencrypt.sh -p /usr/local/etc/synology-letsencrypt/other-example.com\n```\n\nThis creates a separate configuration in\n`/usr/local/etc/synology-letsencrypt/example.com/env` and\n`/usr/local/etc/synology-letsencrypt/other-example.com/env`, respectively. \nYou can then customize each one as needed, including the `hook` file in each configuration.\n\nThis is useful if you need more than one certificate on your Synology or want to generate a certificate for another host managed by the Synology.\n\n### Customizing the hook script\n\nBy default, `synology-letsencrypt.sh` overwrites any changes you make to the hook script to preserve core functionality.\nIf you have customized the hook script, you can preserve your changes by adding the `-c` option when running the command:\n\n```shellsession\n$ /usr/local/bin/synology-letsencrypt.sh -c\n```\n\n## Uninstall\n\nTo **uninstall** `synology-letsencrypt`, run the [uninstall script](uninstall.sh). You can either download and run the script manually, or use the following curl command:\n\n```sh\ncurl -sSL https://raw.githubusercontent.com/JessThrysoee/synology-letsencrypt/master/uninstall.sh | bash\n```\n\n## Consider the [acme-dns](https://github.com/joohoi/acme-dns) project\n\n...if your DNS provider is not _directly_ supported by `lego`, or if you want to avoid storing your DNS provider's API keys on your Synology device. `lego` supports [acme-dns](https://go-acme.github.io/lego/dns/acme-dns/).\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/bash\n\n{\n\narch_map() {\n    local arch\n    arch=$(uname -m)\n    case \"$arch\" in\n        x86_64)       echo \"amd64\" ;;\n        aarch64)      echo \"arm64\" ;;\n        armv7l)       echo \"armv7\" ;;\n        i686|i386)    echo \"386\" ;;\n        *)            return 1 ;;\n    esac\n}\n\nwhile getopts \":a:h\" opt; do\n  case $opt in\n    a) ARCH=\"$OPTARG\";;\n    h) echo \"Usage: $0 [-a <arch>]\"\n       echo \"  -a <arch>  Architecture of lego to install (auto-detect: $(arch_map || echo \"unsupported\"))\"\n       exit 0\n    ;;\n    :) echo \"Error: -${OPTARG} requires an argument.\";;\n    \\?) echo \"Invalid option -$OPTARG\" >&2\n    ;;\n  esac\ndone\n\nif [[ -z $ARCH ]]; then\n    ARCH=$(arch_map) || {\n        echo \"Error: Unsupported architecture '$(uname -m)'. Use -a to specify manually.\" >&2\n        exit 1\n    }\nfi\n\npermissions() {\n    local mod=\"$1\"\n    local path=\"$2\"\n\n    sudo chown root:root \"$path\"\n    sudo chmod \"$mod\" \"$path\"\n}\n\ninstall_lego() {\n    local path=\"/usr/local/bin/lego\"\n    local url\n    \n    url=\"$(\n        curl -sSL \"https://api.github.com/repos/go-acme/lego/releases/latest\" \\\n        | jq --unbuffered -r --arg arch \"$ARCH\" '.assets[].browser_download_url | select(.|endswith(\"linux_\\($arch).tar.gz\"))'\n    )\"\n\n    if [[ -z $url ]]; then\n        echo \"Could not find lego download URL for architecture '$ARCH'! Try a different architecture maybe? See '$0 -h'\" >&2\n        exit 1\n    fi\n\n    curl -sSL \"$url\" \\\n        | sudo tar -zx -C \"${path%/*}\" -- \"${path##*/}\"\n\n    permissions 755 \"$path\"\n    printf \"installed: %s\\n\" \"$path\"\n}\n\ninstall_script() {\n    local name=\"$1\"\n    local path=\"/usr/local/bin/$name\"\n\n    sudo curl -sSL -o \"$path\" \"https://raw.githubusercontent.com/JessThrysoee/synology-letsencrypt/master/$name\"\n\n    permissions 755 \"$path\"\n    printf \"installed: %s\\n\" \"$path\"\n}\n\n\ninstall_configuration() {\n    local dir=\"/usr/local/etc/synology-letsencrypt\"\n    local env=\"$dir/env\"\n\n    sudo mkdir -p \"$dir\"\n    permissions 700 \"$dir\"\n\n    if [[ ! -s $env ]]; then\n        sudo tee \"$env\" > /dev/null <<EOF\nDOMAINS=(--domains \"example.com\" --domains \"*.example.com\")\nEMAIL=\"user@example.com\"\n\n# Specify DNS Provider (this example is from https://go-acme.github.io/lego/dns/simply/)\nDNS_PROVIDER=\"simply\"\nexport SIMPLY_ACCOUNT_NAME=XXXXXXX\nexport SIMPLY_API_KEY=XXXXXXXXXX\nexport SIMPLY_PROPAGATION_TIMEOUT=1800\nexport SIMPLY_POLLING_INTERVAL=30\n\n# Should you need it; additional options can be passed directly to lego\n#LEGO_OPTIONS=(--key-type \"rsa4096\" --server \"https://acme-staging-v02.api.letsencrypt.org/directory\")\n#LEGO_RUN_OPTIONS=()\n#LEGO_RENEW_OPTIONS=(--ari-disable)\nEOF\n    fi\n\n    permissions 600 \"$env\"\n    printf \"installed: %s\\n\" \"$env\"\n    \n    cat << EOF\n    All done!\n\nCheck $env and edit as needed.\nEOF\n}\n\nensure_usr_local_bin() {\n    local dir=\"/usr/local/bin\"\n\n    if [[ ! -d $dir ]]; then\n        mkdir -p \"$dir\"\n        permissions 755 \"$dir\"\n    fi\n}\n\nuninstall_deprecated_script() {\n    local path=\"/usr/local/bin/$1\"\n\n    if [[ -f \"$path\" ]]; then\n        sudo rm \"$path\"\n\n        printf \"uninstalled: %s\\n\" \"$path\"\n    fi\n}\n\ninstall() {\n    ensure_usr_local_bin\n    install_lego\n    install_script \"synology-letsencrypt.sh\"\n    install_script \"synology-letsencrypt-reload-services.sh\"\n    install_script \"synology-letsencrypt-lib.sh\"\n    uninstall_deprecated_script \"synology-letsencrypt-make-cert-id.sh\"\n    install_configuration\n}\n\ninstall\n}\n"
  },
  {
    "path": "synology-letsencrypt-lib.sh",
    "content": "#!/bin/bash -e\n\nmakeCertId() {\n    local cert_id_path=\"$1\"\n    local archive_path=\"$2\"\n\n    local cert_id=\"\"\n    if [[ -s $cert_id_path ]]; then\n        source \"$cert_id_path\"\n    fi\n\n    mkdir -p \"$archive_path\"\n\n    if [[ -z $cert_id ]]; then\n        local archive_cert_path\n        archive_cert_path=$(mktemp -d \"$archive_path\"/XXXXXX)\n        cert_id=\"${archive_cert_path##*/}\"\n        printf 'cert_id=%s' \"$cert_id\" > \"$cert_id_path\"\n    fi\n\n    mkdir -p \"$archive_path/$cert_id\"\n\n    local info=\"$archive_path/INFO\"\n    if [[ -s $info ]]; then\n        local has_cert_id\n        has_cert_id=\"$(jq --arg cert_id \"$cert_id\" 'has($cert_id)' \"$info\")\"\n\n        if [[ $has_cert_id != true ]]; then\n            # append\n            local tmp_info\n            tmp_info=$(mktemp)\n            jq --arg cert_id \"$cert_id\" '.[$cert_id] = { desc: \"\", services: [] }' < \"$info\" > \"$tmp_info\" \\\n                && \\mv \"$tmp_info\" \"$info\"\n        fi\n    else\n        # create\n        jq -n --arg cert_id \"$cert_id\" '{ ($cert_id) : { desc: \"\", services: [] } }' > \"$info\"\n    fi\n}\n\n\n# implement `lego`SanitizedName [ref.: https://github.com/go-acme/lego/blob/f9f9645cf7be7d399c025ec596484263eb9f963a/cmd/internal/storage/storage_certificates.go#L67]\nsanitizedName() {\n    if command -v python3 &>/dev/null; then\n        python3 -c '\nimport sys\n\nname = sys.argv[1]\n\ntry:\n    safe = name.replace(\":\", \"-\").replace(\"*\", \"_\").encode(\"idna\").decode(\"ascii\")\nexcept Exception as e:\n    sys.stderr.write(\"Could not sanitize the name: %s\\n\" % e)\n    sys.exit(1)\n\nout = \"\".join(ch for ch in safe if ch.isalnum() or ch in \"-_.@\")\nsys.stdout.write(out)\n' \"$1\" || return $?\n    else\n        python2 -c '\nimport sys\n\nname = sys.argv[1]\n\ntry:\n    safe = name.decode(\"utf-8\").replace(\":\", \"-\").replace(\"*\", \"_\").encode(\"idna\")\nexcept Exception as e:\n    sys.stderr.write(\"Could not sanitize the name: %s\\n\" % e)\n    sys.exit(1)\n\nout = \"\".join(ch for ch in safe if ch.isalnum() or ch in \"-_.@\")\nsys.stdout.write(out)\n' \"$1\" || return $?\n    fi\n}\n\n"
  },
  {
    "path": "synology-letsencrypt-reload-services.sh",
    "content": "#!/bin/bash -e\n\n[[ $EUID == 0 ]] || { echo >&2 \"This script must be run as root\"; exit 1; }\n\n# Reload services assigned to the certificate with the key `cert_id` in the INFO file.\n# Inspired by https://github.com/bartowl/synology-stuff/blob/master/reload-certs.sh\n\nCERT_ID=\"$1\"\n\nARCHIVE_PATH=\"/usr/syno/etc/certificate/_archive\"\nINFO=\"$ARCHIVE_PATH/INFO\"\n\n\nget() {\n    local i=\"$1\" prop=\"$2\"\n    jq -r --arg cert_id \"$CERT_ID\" --arg i \"$i\" --arg prop \"$prop\" '.[$cert_id].services[$i|tonumber][$prop]' \"$INFO\"\n}\n\nfind_exec_path() {\n    local subscriber=\"$1\"\n\n    # search DSM6 and DSM7 paths\n    for base in /usr/libexec/certificate.d /usr/local/libexec/certificate.d \\\n                /usr/syno/share/certificate.d /usr/local/share/certificate.d\n    do\n        script=\"$base/$subscriber\"\n        if [[ -x \"$script\" ]]; then\n            printf '%s' \"$script\"\n            break\n        fi\n    done\n}\n\nfind_cert_path() {\n    local subscriber=\"$1\" service=\"$2\"\n\n    for base in /usr/local/etc/certificate /usr/syno/etc/certificate; do\n        dir=\"$base/$subscriber/$service\"\n        if [[ -e \"$dir\" ]]; then\n            printf '%s' \"$dir\"\n            break\n        fi\n    done\n}\n\nreload_services() {\n    services_length=$(jq -r --arg cert_id \"$CERT_ID\" '.[$cert_id].services|length' \"$INFO\")\n\n    for (( i = 0; i < services_length; i++ )); do\n\n        subscriber=$(get \"$i\" subscriber)\n        service=$(get \"$i\" service)\n\n        cert_path=\"$(find_cert_path \"$subscriber\" \"$service\")\"\n        if [[ -z $cert_path ]]; then\n            echo >&2 \"cert_path not found in for \\\"$subscriber\\\" \\\"$service\\\"\"\n            continue\n        fi\n\n        if diff -q \"$ARCHIVE_PATH/$CERT_ID/cert.pem\" \"$cert_path/cert.pem\" >/dev/null; then\n            continue # no change\n        fi\n\n        cp \"$ARCHIVE_PATH/$CERT_ID/\"{cert,chain,fullchain,privkey}.pem \"$cert_path/\"\n\n        exec_path=\"$(find_exec_path \"$subscriber\")\"\n        if [[ -x $exec_path ]]; then\n            \"$exec_path\" \"$service\"\n        fi\n\n        profile_exec_script=\"${subscriber}.sh\"\n        if [[ $subscriber == \"system\" && $service == \"default\" ]]; then\n            profile_exec_script=\"dsm.sh\"\n        fi\n        profile_exec_path=\"/usr/libexec/security-profile/tls-profile/$profile_exec_script\"\n        if [[ -x $profile_exec_path ]]; then\n            \"$profile_exec_path\"\n        fi\n    done\n}\n\nreload_services\n\n"
  },
  {
    "path": "synology-letsencrypt.sh",
    "content": "#!/bin/bash -e\n\n[[ $EUID == 0 ]] || { echo >&2 \"This script must be run as root\"; exit 1; }\n\nwhile getopts \":p:ch\" opt; do\n    case $opt in\n        p) LEGO_PATH=\"$OPTARG\" ;;\n        c) CREATE_HOOK=false ;;\n        h)\n            echo \"Usage: $0 [options]\"\n            echo \"  -p <lego_path> The path where Lego will install your certs\"\n            echo \"  -c Suppress [c]reation of the hook scripts, if you have your own\"\n            exit 0\n            ;;\n        :) echo \"Error: -${OPTARG} requires an argument\" >&2 ;;\n        \\?) echo \"Invalid option -$OPTARG\" >&2 ;;\n    esac\ndone\n\nsource /usr/local/bin/synology-letsencrypt-lib.sh\n\nLEGO_PATH=${LEGO_PATH:-/usr/local/etc/synology-letsencrypt}\nCREATE_HOOK=${CREATE_HOOK:-true}\n\nsource \"$LEGO_PATH/env\"\n\nexport LEGO_PATH\n\narchive_path=\"/usr/syno/etc/certificate/_archive\"\ncert_path=\"$LEGO_PATH/certificates\"\nhook_path=\"$LEGO_PATH/hook\"\nmkdir -p \"$cert_path\"\n\ncert_domain=\"$(sanitizedName \"${DOMAINS[1]}\")\"\n\n## cert_id\ncert_id_path=\"$cert_path/$cert_domain.cert_id\"\nmakeCertId \"$cert_id_path\" \"$archive_path\"\nsource \"$cert_id_path\"\n\nif [[ -z $cert_id ]]; then\n    echo >&2 \"ID not found in $cert_id_path\"\n    exit 1\nfi\n\n## install hook\narchive_cert_path=\"$archive_path/$cert_id\"\nif [[ ! -d $archive_cert_path ]]; then\n    mkdir -p \"$archive_cert_path\"\nfi\n\nif [[ ${CREATE_HOOK} == true ]]; then\n    cat >\"$hook_path\" <<EOF\n#!/bin/bash\n\ncp \"${cert_path}/${cert_domain}.crt\" \"${archive_cert_path}/cert.pem\"\ncp \"${cert_path}/${cert_domain}.crt\" \"${archive_cert_path}/fullchain.pem\"\ncp \"${cert_path}/${cert_domain}.issuer.crt\" \"${archive_cert_path}/chain.pem\"\ncp \"${cert_path}/${cert_domain}.key\" \"${archive_cert_path}/privkey.pem\"\n\n/usr/local/bin/synology-letsencrypt-reload-services.sh \"$cert_id\"\nEOF\n\n    chmod 700 \"$hook_path\"\nfi\n\n# https://go-acme.github.io/lego/usage/cli/\n/usr/local/bin/lego run \\\n    --accept-tos \\\n    --deploy-hook \"$hook_path\" \\\n    --key-type \"RSA4096\" \\\n    --email \"$EMAIL\" \\\n    --dns \"$DNS_PROVIDER\" \\\n    \"${DOMAINS[@]}\" \\\n    \"${LEGO_OPTIONS[@]}\"\n\n"
  },
  {
    "path": "uninstall.sh",
    "content": "#!/bin/bash\n\n{\n\nuninstall_script() {\n    local path=\"/usr/local/bin/$1\"\n\n    sudo rm \"$path\"\n\n    printf \"uninstalled: %s\\n\" \"$path\"\n}\n\nuninstall_deprecated_script() {\n    local path=\"/usr/local/bin/$1\"\n\n    if [[ -f \"$path\" ]]; then\n        sudo rm \"$path\"\n\n        printf \"uninstalled: %s\\n\" \"$path\"\n    fi\n}\n\nuninstall_configuration() {\n    local path=\"/usr/local/etc/synology-letsencrypt\"\n\n    sudo rm -r \"$path\"\n\n    printf \"uninstalled configuration: %s\\n\" \"$path\"\n}\n\nuninstall_cert_message() {\n    echo \"\"\n    echo \"Remove any Let's Encrypt certificates from:\"\n    echo \"   Synology DSM -> Control Panel -> Security -> Certificate\"\n    echo \"\"\n}\n\nuninstall() {\n    uninstall_script \"lego\"\n    uninstall_script \"synology-letsencrypt.sh\"\n    uninstall_script \"synology-letsencrypt-reload-services.sh\"\n    uninstall_script \"synology-letsencrypt-lib.sh\"\n    uninstall_deprecated_script \"synology-letsencrypt-make-cert-id.sh\"\n    uninstall_configuration\n    uninstall_cert_message\n}\n\nuninstall\n}\n"
  }
]