Compare commits

..
0 Commits
333 changed files with 2489 additions and 69702 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: acmesh
ko_fi: neilpang
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-27
View File
@@ -1,27 +0,0 @@
<!--
我很忙, 每天可能只有 几秒钟 时间看你的 issue, 如果不按照我的要求写 issue, 你可能不会得到任何回复, 石沉大海.
请确保已经更新到最新的代码, 然后贴上来 `--debug 2` 的调试输出. 没有调试信息. 我做不了什么.
如何调试 https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
If it is a bug report:
- make sure you are able to repro it on the latest released version.
You can install the latest version by: `acme.sh --upgrade`
- Search the existing issues.
- Refer to the [WIKI](https://wiki.acme.sh).
- Debug info [Debug](https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh).
-->
Steps to reproduce
------------------
Debug log
-----------------
```
acme.sh --issue ..... --debug 2
```
-9
View File
@@ -1,9 +0,0 @@
<!--
1. Do NOT send pull request to `master` branch.
Please send to `dev` branch instead.
Any PR to `master` branch will NOT be merged.
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
You will NOT get any review without passing this guide. You also need to fix the CI errors.
-->
-190
View File
@@ -1,190 +0,0 @@
# GitHub Copilot Shell Scripting (sh) Review Instructions for acme.sh
## Overall Goal
Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files) for the [acme.sh](https://github.com/acmesh-official/acme.sh) project. Ensure the code exhibits the highest levels of robustness, security, and portability.
The review must focus on risks unique to Shell scripting, such as proper quoting, robust error handling, and the secure execution of external commands.
## Required Output Format
Organize the feedback into a single, structured report, using the three-level marking system:
1. **Critical Issues (Must Fix Before Merge)**
2. **Suggestions (Improvements to Consider)**
3. **Good Practices (Points to Commend)**
---
## Shell Compatibility
- **POSIX sh only** -- all scripts must target `sh`, not `bash`. No bash-isms allowed.
- **Shebang**: always use `#!/usr/bin/env sh` (not `#!/bin/sh`, not `#!/usr/bin/env bash`).
- **Use `return`, never `exit`** -- scripts are sourced, not executed as subprocesses. `exit` would kill the parent shell.
- **Cross-platform**: code must work on Linux, macOS, FreeBSD, Solaris, and BusyBox environments.
---
## Robustness and Error Handling
- **(Critical)** Enforce the use of the following combination at the start of the script for safety and robustness:
- `set -e`: Exit immediately if a command exits with a non-zero status.
- `set -u`: Treat unset variables as an error and exit.
- `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails.
- **Always check return values** of function calls. If an error occurs, there must be a way to stop execution.
- **Return 1** after `_err` messages:
```sh
if [ -z "$VARIABLE" ]; then
_err "VARIABLE is required"
return 1
fi
```
- Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks.
---
## Security and Quoting
- **(Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**.
- **(Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details.
- Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use.
- Avoid `eval` -- warn against and suggest alternatives, as it can lead to arbitrary code execution.
---
## Use Built-in Helper Functions
Never use raw shell commands when acme.sh provides a wrapper function. This is the most critical rule for portability.
| Instead of | Use |
|---|---|
| `tr '[:upper:]' '[:lower:]'` | `_lower_case()` |
| `tr '[:lower:]' '[:upper:]'` | `_upper_case()` |
| `head -n 1` | `_head_n 1` |
| `openssl dgst` / `openssl` | `_digest()` / `_hmac()` |
| `date` | `_utc_date()` with `sed`/`tr` |
| `curl` / `wget` | `_get()` or `_post()` |
| `sleep` | `_sleep` |
| `base64` / `openssl base64` | `_base64()` |
| `$(( ))` arithmetic | `_math()` |
| `grep -E` / `grep -Po` | `_egrep_o()` |
| `printf` | `echo` |
| `idn` command | `_idn()` / `_is_idn()` |
| `mktemp` | `_mktemp()` |
| `[:space:]` | ` ` |
| `[:alnum:]` | `A-Za-z0-9` |
| `[:alpha:]` | `A-Za-z` |
| `[:digit:]` | `0-9` |
| `awk` | `cut` / `sed` / `while read` loops |
When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted.
---
## Forbidden External Tools
Do not use these commands -- they are not portable across all target platforms:
- `jq` (parse JSON with built-in string manipulation)
- `grep -A` (removed throughout the project)
- `grep -Po` (Perl regex not available everywhere)
- `rev`, `xargs`, `iconv`
- If you must depend on an external tool, check with `_exists` first:
```sh
if ! _exists jq; then
_err "jq is required"
return 1
fi
```
- Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces.
---
## Configuration Management
Use the correct save/read functions depending on hook type:
- **DNS hooks**: `_readaccountconf_mutable` to read API keys, `_saveaccountconf_mutable` to save them. Do not use `_saveaccountconf` or `_readaccountconf`.
- **Deploy hooks**: `_savedeployconf` / `_getdeployconf`
- **Notification hooks**: use account conf functions.
- Save operations should only happen in the correct lifecycle function (e.g., `_issue()`).
- Use environment variables for all configurable values -- do not introduce hardcoded config files.
- Do not clear account conf without a clear reason.
---
## DNS API Conventions
- Read the [DNS API Dev Guide](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide) before writing a DNS plugin.
- Each file under `dnsapi/` must contain a `{filename}_add` function for adding DNS TXT records.
- The `_get_root()` loop counter `i` must start from `1` (not `2`) to support DNS alias mode.
- The `dns_*_rm()` function must remove records **by TXT value**, not by replacing/updating. See [#1261](https://github.com/acmesh-official/acme.sh/issues/1261).
- Preserve the `dns_*_info` metadata variable block in each DNS script header.
---
## Variable Naming
- Use CamelCase with provider prefix: `KINGHOST_Username` (not `KINGHOST_username`).
- Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions.
- Avoid confusingly similar names. Prefer one variable with comma-separated values over multiple variables (e.g., `CZ_Zones` with comma support instead of separate `CZ_Zone` and `CZ_Zones`).
- Do not define variables with the same name in different scopes.
- Variables inside functions should be declared using the `local` keyword to avoid unintentionally modifying global state.
---
## Code Style
- Use `shfmt` for formatting -- CI enforces it.
- Reduce indentation where possible.
- Single space, not double spaces.
- No trailing semicolons after `return` statements.
- Add a newline at the end of every file.
- Use `$(command)` over backticks `` `command` `` for command substitution.
---
## Simplicity
- Prefer hardcoded sensible defaults over unnecessary configuration variables (e.g., use `3600` for TTL instead of a `DESEC_TTL` variable).
- Reject over-engineered solutions. If it can be done in one line, do it in one line.
- Follow existing patterns in the codebase -- new hooks should look like existing hooks.
- Respect user choices: do not `chmod` files that already exist; the user's permissions take priority.
---
## Documentation Requirements
Before a PR can be merged, the following documentation must be provided:
- **Wiki page**: add or update the relevant page:
- DNS APIs: [dnsapi](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) or [dnsapi2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2)
- Deploy hooks: [deployhooks](https://github.com/acmesh-official/acme.sh/wiki/deployhooks)
- Notification hooks: [notify](https://github.com/acmesh-official/acme.sh/wiki/notify)
- Options: [Options-and-Params](https://github.com/acmesh-official/acme.sh/wiki/Options-and-Params)
- **In-code usage**: add usage examples in the help text of `acme.sh` itself.
- **README**: add website URLs for new DNS providers.
---
## CI and Merge Hygiene
- All CI checks must pass before merge.
- Rebase to the latest `dev` branch frequently -- do not use merge commits.
- Enable GitHub Actions on your fork to catch errors early.
- Run the [DNS API Test](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test) workflow for DNS plugins.
- For Docker changes, ensure the Dockerfile includes any required dependencies.
---
## Debug Logging
- Use `_debug2` (not `_debug3` or other levels) unless there is a specific reason for a different level.
---
## Things to Avoid in Reviews
- Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or `shfmt`.
- Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable.
-50
View File
@@ -1,50 +0,0 @@
name: Apache
on:
push:
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Apache:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_APACHE: 1
CASE: le_test_apache
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat apache2
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up Apache
# Apache serves on 5002, which is the HTTP-01 validation port in
# Pebble's default config; acme.sh appends the challenge Alias to
# the main config itself
run: |
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
sudo apache2ctl configtest
sudo systemctl restart apache2
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh
File diff suppressed because it is too large. Load diff
-78
View File
@@ -1,78 +0,0 @@
name: DragonFlyBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/DragonFlyBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/DragonFlyBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
DragonFlyBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
pkg install -y libnghttp2
pkg install -y curl socat
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-82
View File
@@ -1,82 +0,0 @@
name: FreeBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/FreeBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/FreeBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
FreeBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-83
View File
@@ -1,83 +0,0 @@
name: GhostBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/GhostBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/GhostBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
GhostBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
continue-on-error: true
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-85
View File
@@ -1,85 +0,0 @@
name: Haiku
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Haiku:
strategy:
fail-fast: false
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-76
View File
@@ -1,76 +0,0 @@
name: HardenedBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/HardenedBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/HardenedBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
HardenedBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/hardenedbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-76
View File
@@ -1,76 +0,0 @@
name: Hurd
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Hurd:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/hurd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
# Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd
# (the child shell output goes to socat's stdout instead of the socket,
# so clients get an empty reply). Without socat, acme.sh standalone
# mode falls back to its python3 server, which works on Hurd.
prepare: |
apt-get update -y
apt-get install -y curl cron
usesh: true
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-55
View File
@@ -1,55 +0,0 @@
name: Linux
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Linux.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Linux.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Linux:
strategy:
matrix:
os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "gentoo/stage3"]
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_PREFERRED_CHAIN: (STAGING)
TEST_ACME_Server: "LetsEncrypt.org_test"
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: |
cd .. \
&& git clone --depth=1 https://github.com/acmesh-official/acmetest.git \
&& cp -r acme.sh acmetest/
- name: Run acmetest
run: |
cd ../acmetest \
&& ./rundocker.sh testplat ${{ matrix.os }}
-67
View File
@@ -1,67 +0,0 @@
name: MacOS
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/MacOS.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/MacOS.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
MacOS:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: macos-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v6
- name: Install tools
run: brew install socat
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: |
cd .. \
&& git clone --depth=1 https://github.com/acmesh-official/acmetest.git \
&& cp -r acme.sh acmetest/
- name: Run acmetest
run: |
cd ../acmetest \
&& sudo --preserve-env ./letest.sh
-75
View File
@@ -1,75 +0,0 @@
name: MidnightBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/MidnightBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/MidnightBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
MidnightBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: mport install socat curl wget || true
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-77
View File
@@ -1,77 +0,0 @@
name: NetBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/NetBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/NetBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
NetBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-66
View File
@@ -1,66 +0,0 @@
name: Nginx
on:
push:
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Nginx:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_NGINX: 1
CASE: le_test_nginx
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat nginx
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up nginx
# a backend on 8081 plus a site with an aaPanel/BT style
# "location ^~ /" proxy block that shadows plain regex locations
# (regression for #6125); the site listens on 5002, which is the
# HTTP-01 validation port in Pebble's default config
run: |
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
server {
listen 127.0.0.1:8081;
location / {
default_type text/plain;
return 200 "backend";
}
}
server {
listen 5002 default_server;
server_name example.com;
location ^~ / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $http_host;
}
}
EOF
sudo nginx -t
sudo systemctl restart nginx
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh
-86
View File
@@ -1,86 +0,0 @@
name: OPNsense
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OPNsense.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OPNsense.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OPNsense:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/opnsense-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
sync: nfs
run: |
#OPNsense is a firewall appliance whose web GUI holds the 80 port,
#where every --standalone case listens. configd has no "stop"
#action for it and the rc script cannot stop it either, so kill it.
#This belongs here and not in prepare: prepare runs before the
#cache-after-prepare reboot, which would bring the GUI back. And do
#NOT free the port by disabling the GUI's http redirect in
#config.xml: pf's automatic pass rule for the 80 port is generated
#from the web GUI settings, so dropping the redirect also drops the
#rule on the next boot, and the inbound challenge is filtered.
pkill lighttpd || true
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-81
View File
@@ -1,81 +0,0 @@
name: Omnios
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Omnios.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Omnios.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Omnios:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install socat wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-82
View File
@@ -1,82 +0,0 @@
name: OpenBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg_add socat curl wget libnghttp2
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-78
View File
@@ -1,78 +0,0 @@
name: OpenEuler
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenEuler:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openeuler-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
prepare: |
# openEuler ships every repo with both a baseurl and a metalink.
# The metalink mirror pool is split-brain (dl-cdn.openeuler.openatom.cn
# froze at the 2026-08-20 snapshot while repo.openeuler.org moved on),
# so dnf takes repomd.xml from the stale mirror and then 404s fetching
# the checksummed metadata it names from the fresh ones. Keep only the
# vendor baseurl, which is self-consistent.
sed -i '/^metalink=/d' /etc/yum.repos.d/*.repo
dnf install -y curl socat cronie tar gzip
usesh: true
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-81
View File
@@ -1,81 +0,0 @@
name: OpenIndiana
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenIndiana:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install socat curl
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-80
View File
@@ -1,80 +0,0 @@
name: PebbleStrict
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/PebbleStrict.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/PebbleStrict.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
PebbleStrict:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TestingAltDomains: www.example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
Le_HTTPPort: 5002
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_DNS_MANUAL: 1
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat
- name: Run Pebble
run: |
cd ..
curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml
# Pebble reuses a valid authorization in a new order 50% of the time
# by default, which makes the dns manual mode case a coin flip: a
# reused authorization leaves nothing for the TXT record to answer.
printf 'services:\n pebble:\n environment:\n PEBBLE_AUTHZREUSE: "0"\n' >docker-compose.override.yml
docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && ./letest.sh
PebbleStrict_IPCert:
runs-on: ubuntu-latest
env:
TestingDomain: 1.23.45.67
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
Le_HTTPPort: 5002
Le_TLSPort: 5001
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_IPCERT: 1
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat
- name: Run Pebble
run: |
docker run --rm -itd --name=pebble \
-e PEBBLE_VA_ALWAYS_VALID=1 \
-p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest -config /test/config/pebble-config.json -strict
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && ./letest.sh
-83
View File
@@ -1,83 +0,0 @@
name: Solaris
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Solaris.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Solaris.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Solaris:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
pkgutil -U
pkgutil -y -i socat curl wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-80
View File
@@ -1,80 +0,0 @@
name: Tribblix
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Tribblix.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Tribblix.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Tribblix:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: zap install socat curl wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
-103
View File
@@ -1,103 +0,0 @@
name: Ubuntu
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Ubuntu.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Ubuntu.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Ubuntu:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
- TEST_ACME_Server: "ZeroSSL.com"
CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
CA: "ZeroSSL RSA DV SSL CA 2"
CA_EMAIL: "githubtest@acme.sh"
TEST_PREFERRED_CHAIN: ""
- TEST_ACME_Server: "https://localhost:9000/acme/acme/directory"
CA_ECDSA: "Smallstep Intermediate CA"
CA: "Smallstep Intermediate CA"
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: ""
NO_REVOKE: 1
- TEST_ACME_Server: "https://localhost:9000/acme/acme/directory"
CA_ECDSA: "Smallstep Intermediate CA"
CA: "Smallstep Intermediate CA"
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: ""
NO_REVOKE: 1
TEST_IPCERT: 1
TestingDomain: "172.17.0.1"
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
NO_ECC_384: ${{ matrix.NO_ECC_384 }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
NO_REVOKE: ${{ matrix.NO_REVOKE }}
TEST_IPCERT: ${{ matrix.TEST_IPCERT }}
TestingDomain: ${{ matrix.TestingDomain }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat wget
- name: Start StepCA
if: ${{ matrix.TEST_ACME_Server=='https://localhost:9000/acme/acme/directory' }}
run: |
docker run --rm -d \
-p 9000:9000 \
-e "DOCKER_STEPCA_INIT_NAME=Smallstep" \
-e "DOCKER_STEPCA_INIT_DNS_NAMES=localhost,$(hostname -f)" \
-e "DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT=true" \
-e "DOCKER_STEPCA_INIT_PASSWORD=test" \
--name stepca \
smallstep/step-ca:0.23.1
sleep 5
docker exec stepca bash -c "echo test >test" \
&& docker exec stepca step ca provisioner add acme --type ACME --admin-subject step --admin-password-file=/home/step/test \
&& docker exec stepca kill -1 1 \
&& docker exec stepca cat /home/step/certs/root_ca.crt | sudo bash -c "cat - >>/etc/ssl/certs/ca-certificates.crt"
- name: Clone acmetest
run: |
cd .. \
&& git clone --depth=1 https://github.com/acmesh-official/acmetest.git \
&& cp -r acme.sh acmetest/
- name: Run acmetest
run: |
cd ../acmetest \
&& sudo --preserve-env ./letest.sh
-85
View File
@@ -1,85 +0,0 @@
name: Windows
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Windows.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Windows.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Windows:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: windows-latest
env:
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_LOCAL: 1
#The 80 port is used by Windows server, we have to use a custom port, tunnel will also use this port.
Le_HTTPPort: 8888
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v6
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
choco install --no-progress cygwin
shell: cmd
- name: Install cygwin additional packages
run: |
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,xxd
shell: cmd
- name: Set ENV
shell: cmd
run: |
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin;%PATH% >> %GITHUB_ENV%
- name: Check ENV
shell: cmd
run: |
echo "PATH=%PATH%"
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
shell: cmd
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
shell: cmd
run: cd ../acmetest && bash.exe -c ./letest.sh
-114
View File
@@ -1,114 +0,0 @@
name: Blacklist Command
# An issue titled "blacklist: <login-or-email>" opened by the maintainer
# or a write-access member adds that identity to the Blacklist wiki page
# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification
# embeds a prefilled link that opens such an issue in one click.
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
# Share the wiki-guard concurrency group so we never push to the wiki
# at the same time as the guard.
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
blacklist:
# Upstream only: forks have no <fork>.wiki repository to push to.
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:')
runs-on: ubuntu-latest
steps:
- name: Check authorization
id: auth
run: |
assoc="${{ github.event.issue.author_association }}"
case "$assoc" in
OWNER|MEMBER|COLLABORATOR)
echo "ok=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "issue author is not authorized ($assoc); ignoring"
echo "ok=false" >> "$GITHUB_OUTPUT"
;;
esac
- name: Checkout wiki repository
if: steps.auth.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
- name: Add the identity to the blacklist page
if: steps.auth.outputs.ok == 'true'
id: add
env:
TITLE: ${{ github.event.issue.title }}
run: |
target="$(printf '%s' "$TITLE" \
| sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \
| tr 'A-Z' 'a-z')"
case "$target" in
''|*[!a-z0-9._+@-]*)
echo "invalid target: '$target'"
echo "result=invalid" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
echo "target=$target" >> "$GITHUB_OUTPUT"
cd wiki
if [ ! -e Blacklist.md ]; then
echo "result=nopage" >> "$GITHUB_OUTPUT"
exit 0
fi
if grep -Fxiq -- "- $target" Blacklist.md; then
echo "result=already" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -n "$(tail -c1 Blacklist.md)" ]; then
echo >> Blacklist.md
fi
printf -- '- %s\n' "$target" >> Blacklist.md
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Blacklist.md
git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})"
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
echo "result=added" >> "$GITHUB_OUTPUT"
- name: Reply and close
if: steps.auth.outputs.ok == 'true'
uses: actions/github-script@v9
env:
RESULT: ${{ steps.add.outputs.result }}
TARGET: ${{ steps.add.outputs.target }}
with:
script: |
const result = process.env.RESULT;
const target = process.env.TARGET;
const messages = {
added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`,
already: `\`${target}\` is already on the blacklist.`,
invalid: "Could not parse a valid login or email from the issue title.",
nopage: "The Blacklist wiki page does not exist."
};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: messages[result] || "No action taken."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
state_reason: result === "added" ? "completed" : "not_planned"
});
-142
View File
@@ -1,142 +0,0 @@
name: Build DockerHub
on:
push:
branches:
- '*'
tags:
- '*'
paths:
- '**.sh'
- "Dockerfile"
- '.github/workflows/dockerhub.yml'
# A dispatch on a tag ref rebuilds that tag's own image; the weekly
# schedule (default branch only) dispatches the latest release tag so a
# pinned version tag picks up Alpine package security updates (issue 7209).
# master never publishes anything but latest.
schedule:
- cron: '17 3 * * 1'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKER_IMAGE: neilpang/acme.sh
jobs:
CheckToken:
runs-on: ubuntu-latest
outputs:
hasToken: ${{ steps.step_one.outputs.hasToken }}
env:
DOCKER_PASSWORD : ${{ secrets.DOCKER_PASSWORD }}
steps:
- name: Set the value
id: step_one
run: |
if [ "$DOCKER_PASSWORD" ] ; then
echo "hasToken=true" >>$GITHUB_OUTPUT
else
echo "hasToken=false" >>$GITHUB_OUTPUT
fi
- name: Check the value
run: echo ${{ steps.step_one.outputs.hasToken }}
build:
runs-on: ubuntu-latest
needs: CheckToken
if: "github.event_name != 'schedule' && contains(needs.CheckToken.outputs.hasToken, 'true')"
permissions:
contents: read
packages: write
steps:
- name: checkout code
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${DOCKER_IMAGE}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: login to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: login to ghcr
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: build and push the image
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
DOCKER_IMAGE_TAG=${GITHUB_REF#refs/tags/}
fi
if [[ $GITHUB_REF == refs/heads/* ]]; then
DOCKER_IMAGE_TAG=${GITHUB_REF#refs/heads/}
if [[ $DOCKER_IMAGE_TAG == master ]]; then
DOCKER_IMAGE_TAG=latest
AUTO_UPGRADE=1
fi
fi
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
DOCKER_LABELS=()
while read -r label; do
DOCKER_LABELS+=(--label "${label}")
done <<<"${DOCKER_METADATA_OUTPUT_LABELS}"
docker buildx build \
--tag ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
"${DOCKER_LABELS[@]}" \
--output "type=image,push=true" \
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
- name: mirror the image to ghcr (best-effort)
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
rebuild:
# weekly: dispatch this workflow on the latest release tag so the tag
# rebuilds its own image from its own commit and its own workflow file
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
permissions:
contents: read
actions: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: dispatch a rebuild of the latest release tag
run: |
tag="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)"
if [ -z "$tag" ]; then
echo "::error::cannot resolve the latest release tag"
exit 1
fi
echo "dispatching a rebuild of ${tag}"
# A tag cut before this job existed carries a workflow file with no
# workflow_dispatch trigger; the API rejects the dispatch with 422.
# That is expected (nothing to rebuild there), so only a different
# error fails the job.
if ! out="$(gh workflow run dockerhub.yml --repo "${GITHUB_REPOSITORY}" --ref "${tag}" 2>&1)"; then
echo "$out"
case "$out" in
*"does not have 'workflow_dispatch' trigger"*)
echo "::warning::${tag} predates the dispatch trigger; skipping the rebuild"
;;
*)
exit 1
;;
esac
fi
-129
View File
@@ -1,129 +0,0 @@
name: "Update issues"
on:
issues:
types: [opened]
issue_comment:
types: [created]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const item = context.payload.issue || context.payload.pull_request;
// Close on sight anything opened by a user on the wiki Blacklist
// page (maintained by the Wiki Guard workflow).
let blacklist = [];
try {
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
if (res.ok) {
blacklist = (await res.text()).split("\n")
.filter(l => l.startsWith("- "))
.map(l => l.slice(2).trim().toLowerCase())
.filter(Boolean);
}
} catch (e) {
core.warning(`Failed to fetch the blacklist: ${e}`);
}
// A comment on a closed tracking issue reopens it (the standard
// closing note promises this). Bots, blacklisted users and the
// maintainer's own comments don't reopen.
if (context.eventName === "issue_comment") {
const issue = context.payload.issue;
const commenter = context.payload.comment.user;
if (issue.pull_request || issue.state !== "closed") {
return;
}
if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
return;
}
if (commenter.type === "Bot" ||
commenter.login.toLowerCase() === "neilpang" ||
blacklist.includes(commenter.login.toLowerCase())) {
return;
}
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "open"
});
return;
}
if (blacklist.includes(item.user.login.toLowerCase())) {
if (context.payload.pull_request) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: item.number,
state: "closed"
});
} else {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
state: "closed",
state_reason: "not_planned"
});
}
return;
}
if (context.payload.pull_request) {
return;
}
const issue = context.payload.issue;
if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) {
// Handled by the Blacklist / Revert Command workflows.
return;
}
if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
// Tracking issue for a third-party dns/deploy/notify api:
// no upgrade boilerplate; assign it to the opener, label it,
// then close it right away to keep the issue list clean. Any
// later comment reopens it (see the issue_comment handler).
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: [issue.user.login]
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["3rd party api"]
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "completed"
});
return;
}
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
})
-34
View File
@@ -1,34 +0,0 @@
name: Check dns api
on:
pull_request_target:
types:
- opened
paths:
- 'dnsapi/*.sh'
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `**Welcome**
READ ME !!!!!
Read me !!!!!!
First thing: don't send PR to the master branch, please send to the dev branch instead.
Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide).
You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test).
Then reply on this message, otherwise, your code will not be reviewed or merged.
Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
`
})
-31
View File
@@ -1,31 +0,0 @@
name: Check notify api
on:
pull_request_target:
types:
- opened
branches:
- 'dev'
paths:
- 'notify/*.sh'
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `**Welcome**
Please make sure you've read our [Code-of-conduct](../wiki/Code-of-conduct) and add the usage here: [notify](../wiki/notify).
Then reply on this message, otherwise, your code will not be reviewed or merged.
We look forward to reviewing your Pull request shortly ✨
`
})
-110
View File
@@ -1,110 +0,0 @@
name: Revert Command
# An issue titled "revert: <wiki-commit-sha>" opened by the maintainer or
# a write-access member reverts that commit in the wiki repository and
# closes the issue. The wiki-monitor notification embeds a prefilled link
# that opens such an issue in one click.
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
# Share the wiki-guard concurrency group so we never push to the wiki
# at the same time as the guard.
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
revert:
# Upstream only: forks have no <fork>.wiki repository to push to.
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:')
runs-on: ubuntu-latest
steps:
- name: Check authorization
id: auth
run: |
assoc="${{ github.event.issue.author_association }}"
case "$assoc" in
OWNER|MEMBER|COLLABORATOR)
echo "ok=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "issue author is not authorized ($assoc); ignoring"
echo "ok=false" >> "$GITHUB_OUTPUT"
;;
esac
- name: Checkout wiki repository
if: steps.auth.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Revert the wiki commit
if: steps.auth.outputs.ok == 'true'
id: revert
env:
TITLE: ${{ github.event.issue.title }}
run: |
target="$(printf '%s' "$TITLE" \
| sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \
| tr 'A-Z' 'a-z')"
case "$target" in
*[!0-9a-f]*|"")
echo "invalid commit sha: '$target'"
echo "result=invalid" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
echo "target=$target" >> "$GITHUB_OUTPUT"
cd wiki
if ! git cat-file -e "$target^{commit}" 2>/dev/null; then
echo "result=notfound" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git revert --no-edit "$target"; then
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
echo "result=reverted" >> "$GITHUB_OUTPUT"
else
git revert --abort || true
echo "result=conflict" >> "$GITHUB_OUTPUT"
fi
- name: Reply and close
if: steps.auth.outputs.ok == 'true'
uses: actions/github-script@v9
env:
RESULT: ${{ steps.revert.outputs.result }}
TARGET: ${{ steps.revert.outputs.target }}
with:
script: |
const result = process.env.RESULT;
const target = process.env.TARGET;
const messages = {
reverted: `Wiki commit \`${target}\` has been reverted.`,
conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`,
notfound: `Commit \`${target}\` was not found in the wiki repository.`,
invalid: "Could not parse a commit sha from the issue title."
};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: messages[result] || "No action taken."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
state_reason: result === "reverted" ? "completed" : "not_planned"
});
-38
View File
@@ -1,38 +0,0 @@
name: Shellcheck
on:
push:
branches:
- '*'
paths:
- '**.sh'
- '.github/workflows/shellcheck.yml'
pull_request:
branches:
- dev
paths:
- '**.sh'
- '.github/workflows/shellcheck.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ShellCheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Shellcheck
run: sudo apt-get install -y shellcheck
- name: DoShellcheck
run: shellcheck -V && shellcheck -e SC2181 -e SC2089 **/*.sh && echo "shellcheck OK"
shfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install shfmt
run: curl -sSL https://github.com/mvdan/sh/releases/download/v3.1.2/shfmt_v3.1.2_linux_amd64 -o ~/shfmt && chmod +x ~/shfmt
- name: shfmt
run: ~/shfmt -l -w -i 2 . ; git diff --exit-code && echo "shfmt OK"
-44
View File
@@ -1,44 +0,0 @@
name: Mirror version tag
# Historical release tags are plain version numbers ("3.1.3") and cannot be
# renamed. When a plain version tag is pushed (including the tag created by
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
# pointing to the same object, so both forms exist.
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
# refs created with GITHUB_TOKEN do not fire workflows anyway.
on:
push:
tags:
- '[0-9]*'
permissions:
contents: write
jobs:
vtag:
if: github.repository == 'acmesh-official/acme.sh'
runs-on: ubuntu-latest
steps:
- name: Create the v-prefixed tag
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
echo "Tag v$TAG already exists, nothing to do."
exit 0
fi
# Mirror the object the pushed tag actually points at: the commit
# for a lightweight tag, the tag object itself for an annotated or
# signed one. Pointing the mirror at the commit would strip the
# signature, so "git verify-tag v3.1.3" would fail while
# "git verify-tag 3.1.3" succeeds.
sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)"
if [ -z "$sha" ] || [ "$sha" = "null" ]; then
echo "Could not resolve refs/tags/$TAG"
exit 1
fi
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
echo "Created tag v$TAG -> $sha"
-325
View File
@@ -1,325 +0,0 @@
name: Wiki Guard
# Rules enforced here:
# - Only the maintainer and write-access members may delete or rename wiki
# pages. Anyone else doing so gets blacklisted and the page restored to
# its last good revision.
# - Only the maintainer and write-access members may edit the Blacklist
# wiki page. Anyone else touching it gets blacklisted and the page
# reverted.
# - Any wiki change made by a blacklisted identity is reverted.
# A "good" revision is one authored by the maintainer, by this bot, or by
# a non-blacklisted user -- restoring from the deleted commit's parent is
# NOT safe, because vandals replace a page before destroying it and the
# parent would launder their version into a bot commit.
# The gollum event only fires on page create/update, never on deletion,
# so violations are caught by polling the wiki git history.
on:
schedule:
- cron: "*/10 * * * *"
gollum:
# Piggyback on frequent repo activity, because the cron schedule is
# best-effort and often delayed well beyond its interval.
issues:
types: [opened]
issue_comment:
types: [created]
workflow_dispatch:
permissions:
contents: write
issues: write
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
guard:
# Forks have no <fork>.wiki repository, so the checkout below would
# fail there -- run only in the upstream repository.
if: github.repository == 'acmesh-official/acme.sh'
runs-on: ubuntu-latest
steps:
- name: Checkout wiki repository
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Enforce wiki rules
id: guard
env:
# WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate
# members whose write access comes via the organization -- the
# repo-scoped GITHUB_TOKEN only sees direct collaborators.
GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }}
run: |
# Logins with write (push) access to the repository, including
# organization members -- they may delete/rename pages and edit
# the blacklist just like the maintainer. If the API call fails,
# the list stays empty and enforcement falls back to
# maintainer-only, which is the safe direction.
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
echo "write-access members loaded: $(wc -l < writers.txt)"
cd wiki
git config core.quotePath false
# Any author email under this domain is the maintainer.
OWNER_DOMAIN="neilpang.com"
# Our own enforcement commits.
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
BL_PAGE="Blacklist.md"
# Rolling window; the cron runs every 10 minutes, so 7 days gives
# ample overlap without re-judging old changes the maintainer
# already accepted.
WINDOW="7 days ago"
: > ../actions.txt
: > ../bl_new.txt
is_owner() {
case "$1" in
*@"$OWNER_DOMAIN") return 0 ;;
esac
return 1
}
is_bot() {
[ "$1" = "$BOT_EMAIL" ]
}
author_email() {
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
}
# Identity of a commit author: the GitHub login when the email is a
# users.noreply.github.com address, otherwise the email itself.
identity_of() {
case "$1" in
*+*@users.noreply.github.com)
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
;;
*@users.noreply.github.com)
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
;;
*)
printf '%s\n' "$1"
;;
esac
}
is_blacklisted() {
grep -Fxq "$1" ../bl_all.txt
}
# Trusted committers: the maintainer (by email domain), this bot,
# and anyone whose GitHub login has write access to the repo.
is_trusted() {
if is_owner "$1" || is_bot "$1"; then
return 0
fi
grep -Fxq "$(identity_of "$1")" ../writers.txt
}
# Newest commit on file $1 authored by a non-blacklisted user.
last_good_for() {
for g in $(git log --format=%H --no-renames -- "$1"); do
gae="$(author_email "$g")"
if is_trusted "$gae"; then
printf '%s\n' "$g"
return 0
fi
gid="$(identity_of "$gae")"
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
printf '%s\n' "$g"
return 0
fi
done
return 0
}
if [ -e "$BL_PAGE" ]; then
page_existed=1
else
page_existed=""
fi
# ---- 1. Last good version of the blacklist page: the newest
# revision authored by the maintainer or by this bot. Everything
# else on that page is tampering and is discarded.
bl_good_commit=""
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
bl_good_commit="$c"
break
fi
done
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
else
{
echo "# Blacklist"
echo ""
echo "Users listed below violated the wiki rules (deleted or renamed"
echo "pages, or tampered with this page). Their new issues and pull"
echo "requests are closed on sight and their wiki edits are reverted"
echo "automatically. Only the maintainer and write-access members"
echo "may edit this page."
echo ""
echo "To pardon a user while their violation is still inside the"
echo "scan window, replace their entry with: pardon: username"
echo ""
} > ../bl_page.txt
fi
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
bl_add() {
if grep -Fxq "$1" ../bl_pardon.txt; then
return 0
fi
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
printf '%s\n' "$1" >> ../bl_new.txt
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
fi
}
# ---- 2. Blacklist everyone who deleted or renamed a page.
# --no-renames makes a rename count as a deletion of the old path.
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
an="$(git show -s --format=%an "$c")"
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
done
# ---- 3. Blacklist everyone else who touched the blacklist page.
# The revert of their tampering falls out of steps 5 and 6.
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
an="$(git show -s --format=%an "$c")"
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
done
sort -u ../bl_new.txt > ../bl_new_u.txt
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
# ---- 4. Restore pages that are currently missing because a
# non-maintainer deleted them, using the last good revision.
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
| sort -u \
| while IFS= read -r f; do
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
continue
fi
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
if [ -z "$del" ]; then
continue
fi
ae="$(author_email "$del")"
if is_trusted "$ae"; then
continue
fi
good="$(last_good_for "$f")"
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
git checkout "$good" -- "$f"
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
fi
done
# ---- 5. Revert every recent change made by a blacklisted
# identity: each touched file goes back to its newest revision
# authored by a non-blacklisted user; a file that has no such
# revision (they created it) is removed.
if [ -s ../bl_all.txt ]; then
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
id="$(identity_of "$ae")"
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
continue
fi
git show --name-only --no-renames --format= "$c" \
| while IFS= read -r f; do
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
continue
fi
good="$(last_good_for "$f")"
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
want="$(git rev-parse "$good:$f")"
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
if [ "$want" != "$have" ]; then
git checkout "$good" -- "$f"
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
fi
elif [ -e "$f" ]; then
git rm -q -- "$f"
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
fi
done
done
fi
# ---- 6. Regenerate the blacklist page: the last good text plus
# any newly blacklisted identities. This both reverts tampering
# and records new violators; manual edits by the maintainer are
# preserved as the new good text.
cp ../bl_page.txt ../bl_page_new.txt
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
echo >> ../bl_page_new.txt
fi
while IFS= read -r id; do
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
fi
done < ../bl_new_u.txt
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
cp ../bl_page_new.txt "$BL_PAGE"
git add -- "$BL_PAGE"
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
fi
fi
# ---- 7. Commit, push, notify.
if [ -n "$(git status --porcelain)" ]; then
git config user.name "github-actions[bot]"
git config user.email "$BOT_EMAIL"
git commit -m "wiki-guard: restore pages and enforce blacklist"
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
fi
if [ -s ../actions.txt ]; then
{
echo "The wiki guard handled the following rule violations:"
echo ""
cat ../actions.txt
echo ""
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
} > ../guard-msg.txt
echo "acted=true" >> "$GITHUB_OUTPUT"
else
echo "No rule violations found."
echo "acted=false" >> "$GITHUB_OUTPUT"
fi
- name: Create issue to notify Neilpang
if: steps.guard.outputs.acted == 'true'
uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki guard: rule violations handled"
content-filepath: ./guard-msg.txt
assignees: Neilpang
-82
View File
@@ -1,82 +0,0 @@
name: Notify via Issue on Wiki Edit
on:
gollum:
jobs:
notify:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- name: Checkout wiki repository
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Generate wiki change message
id: msg
run: |
actor="${{ github.actor }}"
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH")
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
now="$(date '+%Y-%m-%d %H:%M:%S')"
cd wiki
# Skip notification when the change was authored by the
# maintainer himself (any author email under neilpang.com),
# e.g. a direct git push to the wiki repository.
author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z')
case "$author_email" in
*@neilpang.com)
echo "Change authored by maintainer ($author_email); skipping notification."
echo "notify=false" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
if [ -n "$prev_sha" ]; then
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
else
echo "(no diff)" > ../wiki.diff
fi
cd ..
{
echo "Wiki edited"
echo -n "User: "
echo "@$actor [$actor]($sender_url)"
echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)"
echo "Comment: $page_summary"
echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)"
echo ""
echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)"
echo ""
echo "----"
echo "### diff:"
echo '```diff'
cat wiki.diff
echo '```'
} > wiki-change-msg.txt
echo "notify=true" >> "$GITHUB_OUTPUT"
- name: Create issue to notify Neilpang
if: steps.msg.outputs.notify == 'true'
uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt
assignees: Neilpang
env:
TZ: Asia/Shanghai
-8
View File
@@ -1,8 +0,0 @@
# Contributing
1. Do NOT send pull request to `master` branch.
Please send to `dev` branch instead.
Any PR to `master` branch will NOT be merged.
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
You will NOT get any review without passing this guide. You also need to fix the CI errors.
-96
View File
@@ -1,96 +0,0 @@
FROM alpine:3.23
RUN apk --no-cache add -f \
openssl \
openssh-client \
coreutils \
bind-tools \
curl \
sed \
socat \
tzdata \
oath-toolkit-oathtool \
tar \
libidn \
jq \
yq-go \
supercronic
ENV LE_WORKING_DIR=/acmebin
ENV LE_CONFIG_HOME=/acme.sh
ENV HOME=/acme.sh
ARG AUTO_UPGRADE=1
ENV AUTO_UPGRADE=$AUTO_UPGRADE
#Install
COPY ./acme.sh /install_acme.sh/acme.sh
COPY ./deploy /install_acme.sh/deploy
COPY ./dnsapi /install_acme.sh/dnsapi
COPY ./notify /install_acme.sh/notify
RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H -u 1000 acme
RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/
RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh
RUN chown -R acme:acme $LE_CONFIG_HOME
RUN for verb in help \
version \
install \
uninstall \
upgrade \
issue \
signcsr \
deploy \
install-cert \
renew \
renew-all \
revoke \
remove \
list \
info \
showcsr \
install-cronjob \
uninstall-cronjob \
cron \
toPkcs \
toPkcs8 \
update-account \
register-account \
create-account-key \
create-domain-key \
createCSR \
deactivate \
deactivate-account \
set-notify \
set-default-ca \
set-default-chain \
; do \
printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
; done
RUN printf "%b" '#!'"/usr/bin/env sh\n \
if [ \"\$1\" = \"daemon\" ]; then \n \
if [ ! -f \"\$LE_CONFIG_HOME/crontab\" ]; then \n \
echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \
time=\$(date -u \"+%s\") \n \
random_minute=\$((\$time % 60)) \n \
random_hour=\$((\$time / 60 % 6)) \n \
echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
fi \n \
echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \
exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \
else \n \
exec -- \"\$@\"\n \
fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME
VOLUME /acme.sh
ENTRYPOINT ["/entry.sh"]
CMD ["--help"]
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+160 -584
View File
@@ -1,219 +1,98 @@
<p align="center">
<a href="https://zerossl.com?utm_source=acme-sh">
<picture>
<!-- Dark mode -->
<source
media="(prefers-color-scheme: dark)"
srcset="https://github.com/user-attachments/assets/1308516b-e0cc-496d-b5df-e3932423ead6" />
<!-- Light mode -->
<source
media="(prefers-color-scheme: light)"
srcset="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043" />
<!-- Fallback for environments without media queries -->
<img
alt="ZeroSSL"
src="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043"
height="auto" />
</picture>
</a>
</p>
# An ACME Shell script: acme.sh
- An ACME protocol client written purely in Shell (Unix shell) language.
- Fully ACME protocol implementation.
- Simple, powerful and very easy to use. You only need 3 minutes to learn.
- Bash, dash and sh compatible.
- Simplest shell script for Let's Encrypt free certificate client.
- Purely written in Shell with no dependencies on python or Let's Encrypt official client.
- Just one script, to issue, renew and install your certificates automatically.
- DOES NOT require `root/sudoer` access.
<h1 align="center">🔐 acme.sh</h1>
<h3 align="center">An ACME Protocol Client Written Purely in Shell</h3>
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg" alt="FreeBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg" alt="OpenBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg" alt="NetBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg" alt="MacOS"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg" alt="Ubuntu"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg" alt="Windows"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg" alt="MidnightBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg" alt="GhostBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg" alt="HardenedBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg" alt="OPNsense"></a>
</p>
<p align="center">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg" alt="Shellcheck">
<img src="https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg" alt="PebbleStrict">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg" alt="DockerHub">
</p>
<p align="center">
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" alt="Financial Contributors on Open Collective"></a>
<a href="https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/acme-sh/Lobby.svg" alt="Join the chat at Gitter"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/stars/neilpang/acme.sh.svg" alt="Docker stars"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/pulls/neilpang/acme.sh.svg" alt="Docker pulls"></a>
</p>
It's probably the `easiest&smallest&smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
---
## ✨ Features
- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language
- 📜 Full ACME protocol implementation
- 🔑 Support **ECDSA** certificates
- 🌐 Support **SAN** and **wildcard** certificates
- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn!
- 🔧 Compatible with **Bash**, **dash** and **sh**
- 🚫 No dependencies on Python
- 🔄 One script to issue, renew and install your certificates automatically
- 👤 **DOES NOT** require `root/sudoer` access
- 🐳 Docker ready
- 🌍 IPv6 ready
- 📧 Cron job notifications for renewal or error
> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates.
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/wiki"><strong>📚 Wiki</strong></a> •
<a href="https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker"><strong>🐳 Docker Guide</strong></a> •
<a href="https://twitter.com/neilpangxa"><strong>🐦 Twitter</strong></a>
</p>
---
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
---
## 🏆 Who Uses acme.sh?
- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
- [ruby-china.org](https://ruby-china.org/topics/31983)
- [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management)
- [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89)
- [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
- [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
- [Centminmod](https://centminmod.com/letsencrypt-acmetool-https.html)
- [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
- [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
- [CentOS Web Panel](https://control-webpanel.com)
- [lnmp.org](https://lnmp.org/)
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
---
## 🖥️ Tested OS
Wiki: https://github.com/Neilpang/acme.sh/wiki
#Tested OS
| NO | Status| Platform|
|----|-------|---------|
|1|[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)|Mac OSX
|2|[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)|Windows (cygwin with curl, openssl and crontab included)
|3|[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)|FreeBSD
|4|[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)|Solaris
|5|[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)| Ubuntu
|6|NA|pfsense
|7|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD
|8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
|9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
|10|[![MidnightBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD
|11|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|12|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|21|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
|30|[![HardenedBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml)|HardenedBSD
|31|[![OPNsense](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml)|OPNsense
|1|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/ubuntu-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Ubuntu
|2|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/debian-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Debian
|3|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/centos-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|CentOS
|4|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/windows-cygwin.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Windows (cygwin with curl, openssl and crontab included)
|5|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/freebsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|FreeBSD
|6|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/pfsense.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|pfsense
|7|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/opensuse-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|openSUSE
|8|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/alpine-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Alpine Linux (with curl)
|9|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/base-archlinux.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Archlinux
|10|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/fedora-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|fedora
|11|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/kalilinux-kali-linux-docker.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Kali Linux
|12|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/oraclelinux-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Oracle Linux
|13|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/proxmox.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Proxmox https://pve.proxmox.com/wiki/HTTPSCertificateConfiguration#Let.27s_Encrypt_using_acme.sh
|14|-----| Cloud Linux https://github.com/Neilpang/le/issues/111
|15|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/openbsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|OpenBSD
|16|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/mageia.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Mageia
For all build statuses, check our [daily build project](https://github.com/Neilpang/acmetest):
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
>
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
https://github.com/Neilpang/acmetest
---
# Supported Mode
## 🏛️ Supported CA
1. Webroot mode
2. Standalone mode
3. Apache mode
4. Dns mode
| CA | Status |
|---|---|
| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** |
| Letsencrypt.org CA | ✅ Supported |
| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported |
| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported |
| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported |
| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported |
| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported |
# Upgrade from 1.x to 2.x
---
You can simply uninstall 1.x and re-install 2.x.
2.x is 100% compatible to 1.x. You will feel right at home as if nothing has changed.
## ⚙️ Supported Modes
# le.sh renamed to acme.sh NOW!
| Mode | Description |
|------|-------------|
| 📁 Webroot mode | Use existing webroot directory |
| 🖥️ Standalone mode | Built-in webserver on port 80 |
| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 |
| 🪶 Apache mode | Use Apache for verification |
| ⚡ Nginx mode | Use Nginx for verification |
| 🌐 DNS mode | Use DNS TXT records |
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
All configurations are 100% compatible between `le.sh` and `acme.sh`. You just need to uninstall `le.sh` and re-install `acme.sh` again.
Nothing will be broken during the process.
---
# How to install
## 📖 Usage Guide
### 1. Install online:
### 1️⃣ How to Install
#### 📥 Install Online
> Check this project: https://github.com/acmesh-official/get.acme.sh
Check this project: https://github.com/Neilpang/get.acme.sh
```bash
curl https://get.acme.sh | sh -s email=my@example.com
curl https://get.acme.sh | sh
```
**Or:**
Or:
```bash
wget -O - https://get.acme.sh | sh -s email=my@example.com
wget -O - https://get.acme.sh | sh
```
#### 📦 Install from Git
Clone this project and launch installation:
### 2. Or, Install from git:
Clone this project:
```bash
git clone https://github.com/acmesh-official/acme.sh.git
git clone https://github.com/Neilpang/acme.sh.git
cd ./acme.sh
./acme.sh --install -m my@example.com
./acme.sh --install
```
> 💡 You `don't have to be root` then, although `it is recommended`.
You `don't have to be root` then, although `it is recommended`.
📚 **Advanced Installation:** https://github.com/acmesh-official/acme.sh/wiki/How-to-install
Advanced Installation: https://github.com/Neilpang/acme.sh/wiki/How-to-install
**The installer will perform 3 actions:**
The installer will perform 3 actions:
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
All certs will be placed in this folder too.
2. Create alias for: `acme.sh=~/.acme.sh/acme.sh`.
3. Create daily cron job to check and renew the certs if needed.
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
All certs will be placed in this folder.
2. Create alias for: `acme.sh=~/.acme.sh/acme.sh`.
3. Create everyday cron job to check and renew the cert if needed.
Cron entry example:
@@ -221,509 +100,206 @@ Cron entry example:
0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null
```
> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect.
After the installation, you must close current terminal and reopen again to make the alias take effect.
✅ **You are ready to issue certs now!**
Ok, you are ready to issue cert now.
Show help message:
**Show help message:**
```sh
acme.sh -h
```
#### 🔏 Verify a Release
root@v1:~# acme.sh -h
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
signing happens on the maintainer's machine, so the private key is never
available to CI. The public half is [`allowed_signers`](allowed_signers) in
this repository. From a clone:
```bash
git config gpg.ssh.allowedSignersFile allowed_signers
```
```bash
git verify-tag 3.1.5
```
The signature covers the tag object, which pins the commit and therefore the
whole tree, so a good signature verifies every file at that release and no
separate tarball checksum is needed. Build a tarball from the verified tag:
```bash
git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
```
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
---
### 2️⃣ Issue a Certificate
# Just issue a cert:
**Example 1:** Single domain.
```bash
acme.sh --issue -d example.com -w /home/wwwroot/example.com
```
or:
```bash
acme.sh --issue -d example.com -w /home/username/public_html
```
or:
```bash
acme.sh --issue -d example.com -w /var/www/html
acme.sh --issue -d aa.com -w /home/wwwroot/aa.com
```
**Example 2:** Multiple domains in the same cert.
```bash
acme.sh --issue -d example.com -d www.example.com -d cp.example.com -w /home/wwwroot/example.com
acme.sh --issue -d aa.com -d www.aa.com -d cp.aa.com -w /home/wwwroot/aa.com
```
The parameter `/home/wwwroot/example.com` or `/home/username/public_html` or `/var/www/html` is the web root folder where you host your website files. You **MUST** have `write access` to this folder.
The parameter `/home/wwwroot/aa.com` is the web root folder. You **MUST** have `write access` to this folder.
Second argument **"example.com"** is the main domain you want to issue the cert for.
You must have at least one domain there.
Second argument **"aa.com"** is the main domain you want to issue cert for.
You must have at least a domain there.
You must point and bind all the domains to the same webroot dir: `/home/wwwroot/example.com`.
You must point and bind all the domains to the same webroot dir: `/home/wwwroot/aa.com`.
The certs will be placed in `~/.acme.sh/example.com/`
Generate/issued certs will be placed in `~/.acme.sh/aa.com/`
> 🔄 The certs will be renewed automatically every **30** days.
The issued cert will be renewed every 80 days automatically.
> 🔐 The certs will default to **ECC** certificates.
More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# Install issued cert to apache/nginx etc.
### 3️⃣ Install the Certificate to Apache/Nginx
After you issue a cert, you probably want to install the cert with your nginx/apache or other servers you may be using.
After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers.
> ⚠️ **IMPORTANT:** You **MUST** use this command to copy the certs to the target files. **DO NOT** use the certs files in `~/.acme.sh/` folder — they are for internal use only, the folder structure may change in the future.
#### 🪶 Apache Example:
```bash
acme.sh --install-cert -d example.com \
--cert-file /path/to/certfile/in/apache/cert.pem \
--key-file /path/to/keyfile/in/apache/key.pem \
--fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \
--reloadcmd "service apache2 force-reload"
```
#### ⚡ Nginx Example:
```bash
acme.sh --install-cert -d example.com \
--key-file /path/to/keyfile/in/nginx/key.pem \
--fullchain-file /path/to/fullchain/nginx/cert.pem \
--reloadcmd "service nginx force-reload"
acme.sh --installcert -d aa.com \
--certpath /path/to/certfile/in/apache/nginx \
--keypath /path/to/keyfile/in/apache/nginx \
--capath /path/to/ca/certfile/apache/nginx \
--fullchainpath path/to/fullchain/certfile/apache/nginx \
--reloadcmd "service apache2|nginx reload"
```
Only the domain is required, all the other parameters are optional.
The ownership and permission info of existing files are preserved. You can pre-create the files to define the ownership and permission.
Install the issued cert/key to the production apache or nginx path.
Install/copy the cert/key to the production Apache or Nginx path.
The cert will be `renewed every 80 days by default` (which is configurable). Once the cert is renewed, the apache/nginx will be automatically reloaded by the command: `service apache2 reload` or `service nginx reload`.
> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
# Use Standalone server to issue cert
> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert.
**(requires you be root/sudoer, or you have permission to listen tcp 80 port)**
---
### 4️⃣ Use Standalone Server to Issue Certificate
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
The tcp `80` port **MUST** be free to listen, otherwise you will be prompted to free the `80` port and try again.
```bash
acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
acme.sh --issue --standalone -d aa.com -d www.aa.com -d cp.aa.com
```
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
---
# Use Standalone tls server to issue cert
### 5️⃣ Use Standalone TLS Server to Issue Certificate
**(requires you be root/sudoer, or you have permission to listen tcp 443 port)**
> 🔐 Requires root/sudoer or permission to listen on port **443** (TCP)
acme.sh supports `tls-sni-01` validation.
> ⚠️ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
The tcp `443` port **MUST** be free to listen, otherwise you will be prompted to free the `443` port and try again.
```bash
acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
acme.sh --issue --tls -d aa.com -d www.aa.com -d cp.aa.com
```
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
---
# Use Apache mode
### 6️⃣ Use Apache Mode
**(requires you be root/sudoer, since it is required to interact with apache server)**
> 🔐 Requires root/sudoer to interact with Apache server
If you are running a web server, apache or nginx, it is recommended to use the `Webroot mode`.
If you are running a web server, it is recommended to use the `Webroot mode`.
Particularly, if you are running an apache server, you should use apache mode instead. This mode doesn't write any files to your web root folder.
Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
Just set string "apache" as the second argument, it will force use of apache plugin automatically.
```sh
acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
```
acme.sh --issue --apache -d aa.com -d www.aa.com -d user.aa.com
```
> 💡 **Note:** This Apache mode is only to issue the cert, it will **not** change your Apache config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Apache server, don't worry!
More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
# Use DNS mode:
---
### 7️⃣ Use Nginx Mode
> 🔐 Requires root/sudoer to interact with Nginx server
If you are running a web server, it is recommended to use the `Webroot mode`.
Particularly, if you are running an Nginx server, you can use Nginx mode instead. This mode doesn't write any files to your web root folder.
It will configure Nginx server automatically to verify the domain and then restore the Nginx config to the original version. So, the config is not changed.
```sh
acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
```
> 💡 **Note:** This Nginx mode is only to issue the cert, it will **not** change your Nginx config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Nginx server, don't worry!
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
### 8️⃣ Automatic DNS API Integration
If your DNS provider supports API access, we can use that API to automatically issue the certs.
> ✨ **You don't have to do anything manually!**
📚 **Currently acme.sh supports most DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi
---
### 9️⃣ Use DNS Manual Mode
See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first.
If your dns provider doesn't support any api access, you can add the txt record by hand.
Support the `dns-01` challenge.
```bash
acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com
acme.sh --issue --dns -d aa.com -d www.aa.com -d user.aa.com
```
You should get an output like below:
You should get the output like below:
```sh
```
Add the following txt record:
Domain:_acme-challenge.example.com
Domain:_acme-challenge.aa.com
Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c
Add the following txt record:
Domain:_acme-challenge.www.example.com
Domain:_acme-challenge.www.aa.com
Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Please add those txt records to the domains. Waiting for the dns to take effect.
```
Then just rerun with `renew` argument:
```bash
acme.sh --renew -d example.com
acme.sh --renew -d aa.com
```
✅ **Done!**
Ok, it's finished.
> ⚠️ **WARNING:** This is DNS manual mode — it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.**
# Automatic DNS API integration
---
If your DNS provider supports API access, we can use API to automatically issue the certs.
### 🔟 Use DNS Persist Mode
You don't have do anything manually!
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
### Currently acme.sh supports:
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
1. Cloudflare.com API
2. Dnspod.cn API
3. Cloudxns.com API
4. AWS Route 53, see: https://github.com/Neilpang/acme.sh/issues/65
5. lexicon dns api: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
(DigitalOcean, DNSimple, DnsMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
##### More APIs are coming soon...
#### 🪄 Step 1: Print the TXT record value
If your DNS provider is not on the supported list above, you can write your own script API easily. If you do please consider submitting a [Pull Request](https://github.com/Neilpang/acme.sh/pulls) and contribute to the project.
For more details: [How to use dns api](dnsapi)
# Issue ECC certificate:
`Let's Encrypt` now can issue **ECDSA** certificates.
And we also support it.
Just set the `length` parameter with a prefix `ec-`.
For example:
### Single domain ECC cerfiticate:
```bash
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
acme.sh --issue -w /home/wwwroot/aa.com -d aa.com --keylength ec-256
```
Options:
| Flag | Description |
|------|-------------|
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
You should get an output like:
```sh
TXT persist domain:_validation-persist.example.com
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
```
#### ✍️ Step 2: Add the TXT record to your DNS
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
#### 📜 Step 3: Issue the certificate
SAN multi domain ECC certificate:
```bash
acme.sh --issue -d example.com --dns-persist
acme.sh --issue -w /home/wwwroot/aa.com -d aa.com -d www.aa.com --keylength ec-256
```
✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
Please look at the last parameter above.
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
Valid values are:
---
1. **ec-256 (prime256v1, "ECDSA P-256")**
2. **ec-384 (secp384r1, "ECDSA P-384")**
3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA)
# Under the Hood
Just set the `keylength` to a valid, supported value.
Speak ACME language using shell, directly to "Let's Encrypt".
**Valid values for the `keylength` parameter:**
TODO:
| Key Length | Description |
|------------|-------------|
| `ec-256` | prime256v1, "ECDSA P-256" ⭐ **Default** |
| `ec-384` | secp384r1, "ECDSA P-384" |
| `ec-521` | secp521r1, "ECDSA P-521" ⚠️ Not supported by Let's Encrypt yet |
| `2048` | RSA 2048-bit |
| `3072` | RSA 3072-bit |
| `4096` | RSA 4096-bit |
# Acknowledgment
1. Acme-tiny: https://github.com/diafygi/acme-tiny
2. ACME protocol: https://github.com/ietf-wg-acme/acme
3. Certbot: https://github.com/certbot/certbot
**Examples:**
# License & Other
#### Single domain with ECDSA P-384 certificate
License is GPLv3
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
```
Please Star and Fork me.
#### SAN multi domain with RSA4096 certificate
[Issues](https://github.com/Neilpang/acme.sh/issues) and [pull requests](https://github.com/Neilpang/acme.sh/pulls) are welcomed.
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
```
---
### 1️⃣2️⃣ Issue Wildcard Certificates
It's simple! Just give a wildcard domain as the `-d` parameter:
```sh
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
```
---
### 1️⃣3️⃣ How to Renew Certificates
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
However, you can force a renewal:
```sh
acme.sh --renew -d example.com --force
```
**For ECC cert:**
```sh
acme.sh --renew -d example.com --force --ecc
```
#### 📡 ACME Renewal Information (ARI) — RFC 9773
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
| What | When | Why |
|------|------|-----|
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
1. `--force` is given
2. The CA's **ARI `suggestedWindow` has started**
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
You can see the resulting next renewal time (already ARI-picked when applicable) in:
```sh
acme.sh --info -d example.com
# Look for: Le_NextRenewTimeStr=...
```
For the live ARI window the CA is currently advertising, run with `--debug 2`:
```sh
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
```
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
---
### 1️⃣4️⃣ How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
```sh
acme.sh --remove -d example.com [--ecc]
```
The cert/key file is not removed from the disk.
> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually.
---
### 1️⃣5️⃣ How to Upgrade acme.sh
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
**Update to latest:**
```sh
acme.sh --upgrade
```
**Enable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade
```
**Disable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade 0
```
---
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
---
### 1️⃣7️⃣ Send Notifications in Cronjob
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
---
### 1️⃣8️⃣ Under the Hood
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
---
### 1️⃣9️⃣ Acknowledgments
| Project | Link |
|---------|------|
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
---
## 👥 Contributors
### 💻 Code Contributors
This project exists thanks to all the people who contribute.
<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
### 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
#### 👤 Individuals
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
#### 🏢 Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)]
<a href="https://opencollective.com/acmesh/organization/0/website"><img src="https://opencollective.com/acmesh/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/1/website"><img src="https://opencollective.com/acmesh/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/2/website"><img src="https://opencollective.com/acmesh/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/3/website"><img src="https://opencollective.com/acmesh/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/4/website"><img src="https://opencollective.com/acmesh/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/5/website"><img src="https://opencollective.com/acmesh/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/6/website"><img src="https://opencollective.com/acmesh/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/7/website"><img src="https://opencollective.com/acmesh/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/8/website"><img src="https://opencollective.com/acmesh/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/9/website"><img src="https://opencollective.com/acmesh/organization/9/avatar.svg"></a>
---
### 2️⃣0️⃣ License & Others
📄 **License:** GPLv3
⭐ Please **Star** and **Fork** this project!
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
---
### 2️⃣1️⃣ Donate
> 💝 Your donation makes **acme.sh** better!
| Method | Link |
|--------|------|
| PayPal / Alipay(支付宝) / Wechat(微信) | [https://donate.acme.sh/](https://donate.acme.sh/) |
📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
---
### 2️⃣2️⃣ About This Repository
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com.
>
> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL.
<p align="center">
<a href="https://zerossl.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://zerossl.com/assets/images/zerossl_logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://zerossl.com/assets/images/zerossl_logo.svg">
<img src="https://zerossl.com/assets/images/zerossl_logo.svg" alt="ZeroSSL" width="256">
</picture>
</a>
</p>
+1744 -8665
View File
File diff suppressed because it is too large. Load diff
-341
View File
@@ -1,341 +0,0 @@
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
#
# "acme.sh --install" copies this file to the acme.sh home dir and wires
# it into acme.sh.env, so the completion is loaded automatically in new
# bash sessions after installation.
#
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
# it to /usr/share/bash-completion/completions/acme.sh
#
# Zsh users can load it with:
# autoload -U +X bashcompinit && bashcompinit
# . /path/to/acme.sh.completion
# This file may also be sourced by non-bash shells via acme.sh.env,
# so silently do nothing if the "complete" builtin is not available.
if ! command -v complete >/dev/null 2>&1; then
return 0 2>/dev/null || exit 0
fi
# Add each word of $1 that starts with $cur to COMPREPLY.
# The words are read line by line, so that candidates like a wildcard
# domain "*.example.com" are never glob-expanded against the cwd.
_acme_sh_add_matches() {
local _word
while read -r _word; do
[ -n "$_word" ] || continue
case "$_word" in
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
esac
done <<EOF
$(printf '%s\n' "$1" | tr ' ' '\n')
EOF
return 0
}
_acme_sh_files() {
local _file
while IFS= read -r _file; do
[ -n "$_file" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_file")
done <<EOF
$(compgen -f -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
_acme_sh_dirs() {
local _dir
while IFS= read -r _dir; do
[ -n "$_dir" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
done <<EOF
$(compgen -d -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
# Complete the domains that already have a cert: every directory in the
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
_acme_sh_domains() {
local _dir _name _domains=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _dir in "$_acme_conf_home"/*/; do
[ -d "$_dir" ] || continue
_name="${_dir%/}"
_name="${_name##*/}"
_name="${_name%_ecc}"
if [ -f "${_dir}${_name}.conf" ]; then
case " $_domains " in
*" $_name "*) ;;
*) _domains="$_domains $_name" ;;
esac
fi
done
_acme_sh_add_matches "$_domains"
}
# Complete hook names from a subfolder of the acme.sh home dir.
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
_acme_sh_hooks() {
local _file _hooks=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _file in "$_acme_home/$1/$2"*.sh; do
[ -f "$_file" ] || continue
_file="${_file##*/}"
_hooks="$_hooks ${_file%.sh}"
done
_acme_sh_add_matches "$_hooks"
}
_acme_sh_completion() {
local cur prev _acme_home _acme_conf_home
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev=""
if [ "$COMP_CWORD" -gt 0 ]; then
prev="${COMP_WORDS[COMP_CWORD - 1]}"
fi
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
# The first argument is the command.
if [ "$COMP_CWORD" -eq 1 ]; then
_acme_sh_add_matches "
--help
--version
--install
--install-online
--uninstall
--upgrade
--issue
--deploy
--sign-csr
--show-csr
--install-cert
--renew
--renew-all
--revoke
--remove
--list
--list-profiles
--info
--to-pkcs12
--to-pkcs8
--create-account-key
--create-domain-key
--create-csr
--deactivate
--update-account
--register-account
--deactivate-account
--make-dns-persist-value
--install-cronjob
--uninstall-cronjob
--cron
--set-notify
--set-default-ca
--set-default-chain
"
return 0
fi
# Complete the value of the previous option.
case "$prev" in
-d | --domain | --challenge-alias | --domain-alias)
_acme_sh_domains
return 0
;;
--dns)
# The dns hook argument is optional, keep completing options if the
# current word already looks like one.
case "$cur" in
-*) ;;
*)
_acme_sh_hooks "dnsapi" "dns_"
return 0
;;
esac
;;
--deploy-hook)
_acme_sh_hooks "deploy" ""
return 0
;;
--notify-hook)
_acme_sh_hooks "notify" ""
return 0
;;
--server)
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
return 0
;;
-k | --keylength | -ak | --accountkeylength)
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
return 0
;;
--debug)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
esac
;;
--log)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--nginx)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--auto-upgrade | --always-force-new-domain-key)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1"
return 0
;;
esac
;;
--log-level)
_acme_sh_add_matches "1 2"
return 0
;;
--syslog)
_acme_sh_add_matches "0 3 6 7"
return 0
;;
--notify-level)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
--notify-mode)
_acme_sh_add_matches "0 1"
return 0
;;
--revoke-reason)
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
return 0
;;
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
_acme_sh_files
return 0
;;
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
_acme_sh_dirs
return 0
;;
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
# These options take a free-form value, offer nothing.
return 0
;;
esac
# Complete the parameters.
_acme_sh_add_matches "
--accountconf
--accountkey
--accountkeylength
--alpn
--always-force-new-domain-key
--apache
--auto-upgrade
--branch
--ca-bundle
--ca-file
--ca-path
--cert-file
--cert-home
--cert-profile
--challenge-alias
--config-home
--csr
--days
--debug
--deploy-hook
--dns
--dns-persist
--dns-persist-ca-name
--dns-persist-days
--dns-persist-wildcard
--dnssleep
--domain
--domain-alias
--eab-hmac-key
--eab-kid
--ecc
--email
--extended-key-usage
--force
--force-color
--fullchain-file
--home
--httpport
--insecure
--key-file
--keylength
--listen-v4
--listen-v6
--listraw
--local-address
--log
--log-level
--nginx
--no-color
--no-cron
--no-profile
--notify-hook
--notify-level
--notify-mode
--notify-source
--ocsp-must-staple
--openssl-bin
--output-insecure
--password
--post-hook
--pre-hook
--preferred-chain
--reloadcmd
--renew-hook
--revoke-reason
--server
--staging
--standalone
--stateless
--stop-renew-on-error
--syslog
--tlsport
--treat-skip-as-success
--use-wget
--useragent
--valid-from
--valid-to
--webroot
--yes-I-know-dns-manual-mode-enough-go-ahead-please
"
return 0
}
complete -F _acme_sh_completion acme.sh
-20
View File
@@ -1,20 +0,0 @@
# acme.sh release signing key.
#
# Release tags are signed with this key. Its private half is held by the
# maintainer and is never available to CI, so a compromise of the build
# pipeline cannot produce a tag that verifies against this file.
#
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
#
# To verify a release tag, from a clone of this repository:
#
# git config gpg.ssh.allowedSignersFile allowed_signers
# git verify-tag 3.1.5
#
# A good signature covers the tag object, which pins the commit, which pins
# the whole tree -- so verifying the tag verifies every file at that
# release. Build a tarball from the verified tag with:
#
# git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
#
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
-6
View File
@@ -1,6 +0,0 @@
# Using deploy api
deploy hook usage:
https://github.com/acmesh-official/acme.sh/wiki/deployhooks
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
# Script to create certificate to Alibaba Cloud CDN
#
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun
#
# This deployment required following variables
# export Ali_Key="ALIACCESSKEY"
# export Ali_Secret="ALISECRETKEY"
# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi
#
# To specify the CDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates
# export DEPLOY_ALI_CDN_DOMAIN="cdn.example.com"
# If you have multiple CDN domains using the same certificate, just
# export DEPLOY_ALI_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
#
# For DCDN, see ali_dcdn deploy hook
Ali_CDN_API="https://cdn.aliyuncs.com/"
ali_cdn_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Load dnsapi/dns_ali.sh to reduce the duplicated codes
# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276
dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)"
# shellcheck source=/dev/null
if ! . "$dnsapi_ali"; then
_err "Error loading file $dnsapi_ali. Please check your API file and try again."
return 1
fi
_prepare_ali_credentials || return 1
_getdeployconf DEPLOY_ALI_CDN_DOMAIN
if [ "$DEPLOY_ALI_CDN_DOMAIN" ]; then
_savedeployconf DEPLOY_ALI_CDN_DOMAIN "$DEPLOY_ALI_CDN_DOMAIN"
else
DEPLOY_ALI_CDN_DOMAIN="$_cdomain"
fi
# read cert and key files and urlencode both
_cert=$(_url_encode upper-hex <"$_cfullchain")
_key=$(_url_encode upper-hex <"$_ckey")
_debug2 _cert "$_cert"
_debug2 _key "$_key"
## update domain ssl config
for domain in $DEPLOY_ALI_CDN_DOMAIN; do
_set_cdn_domain_ssl_certificate_query "$domain" "$_cert" "$_key"
if _ali_rest "Set CDN domain SSL certificate for $domain" "" POST; then
_info "Domain $domain certificate has been deployed successfully"
fi
done
return 0
}
# domain pub pri
_set_cdn_domain_ssl_certificate_query() {
endpoint=$Ali_CDN_API
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=SetCdnDomainSSLCertificate'
query=$query'&CertType=upload'
query=$query'&DomainName='$1
query=$query'&Format=json'
query=$query'&SSLPri='$3
query=$query'&SSLProtocol=on'
query=$query'&SSLPub='$2
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-05-10'
}
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
# Script to create certificate to Alibaba Cloud DCDN
#
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun
#
# This deployment required following variables
# export Ali_Key="ALIACCESSKEY"
# export Ali_Secret="ALISECRETKEY"
# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi
#
# To specify the DCDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates
# export DEPLOY_ALI_DCDN_DOMAIN="dcdn.example.com"
# If you have multiple CDN domains using the same certificate, just
# export DEPLOY_ALI_DCDN_DOMAIN="dcdn1.example.com dcdn2.example.com"
#
# For regular CDN, see ali_cdn deploy hook
Ali_DCDN_API="https://dcdn.aliyuncs.com/"
ali_dcdn_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Load dnsapi/dns_ali.sh to reduce the duplicated codes
# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276
dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)"
# shellcheck source=/dev/null
if ! . "$dnsapi_ali"; then
_err "Error loading file $dnsapi_ali. Please check your API file and try again."
return 1
fi
_prepare_ali_credentials || return 1
_getdeployconf DEPLOY_ALI_DCDN_DOMAIN
if [ "$DEPLOY_ALI_DCDN_DOMAIN" ]; then
_savedeployconf DEPLOY_ALI_DCDN_DOMAIN "$DEPLOY_ALI_DCDN_DOMAIN"
else
DEPLOY_ALI_DCDN_DOMAIN="$_cdomain"
fi
# read cert and key files and urlencode both
_cert=$(_url_encode upper-hex <"$_cfullchain")
_key=$(_url_encode upper-hex <"$_ckey")
_debug2 _cert "$_cert"
_debug2 _key "$_key"
## update domain ssl config
for domain in $DEPLOY_ALI_DCDN_DOMAIN; do
_set_dcdn_domain_ssl_certificate_query "$domain" "$_cert" "$_key"
if _ali_rest "Set DCDN domain SSL certificate for $domain" "" POST; then
_info "Domain $domain certificate has been deployed successfully"
fi
done
return 0
}
# domain pub pri
_set_dcdn_domain_ssl_certificate_query() {
endpoint=$Ali_DCDN_API
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=SetDcdnDomainSSLCertificate'
query=$query'&CertType=upload'
query=$query'&DomainName='$1
query=$query'&Format=json'
query=$query'&SSLPri='$3
query=$query'&SSLProtocol=on'
query=$query'&SSLPub='$2
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-01-15'
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to apache server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
apache_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "Deploy cert to apache server, Not implemented yet"
return 1
}
-222
View File
@@ -1,222 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
# Deploy hook: Baidu Cloud CDN
#
# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol
#
# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w
#
# Uses the same credential variables as dnsapi/dns_baidu.sh:
# export Baidu_AK="your-access-key-id"
# export Baidu_SK="your-secret-access-key"
#
# To deploy to a CDN domain different from the certificate CN
# (e.g. wildcard or multi-domain certs):
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com"
#
# Multiple CDN domains sharing the same certificate:
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
BAIDU_CDN_HOST="cdn.baidubce.com"
_BAIDU_CDN_BCE_AUTH_RESULT=""
baidu_cdn_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _baidu_cdn_load_credentials; then
return 1
fi
_getdeployconf DEPLOY_BAIDU_CDN_DOMAIN
if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then
_savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN"
else
DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain"
fi
# Build JSON "domains" array from space-separated domain list
_domains_json=""
for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do
_d_e="$(_baidu_cdn_json_escape "$_d")"
if [ -z "$_domains_json" ]; then
_domains_json="\"${_d_e}\""
else
_domains_json="${_domains_json},\"${_d_e}\""
fi
done
# Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars
_cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)"
case "$_cert_name" in
[A-Za-z]*) ;;
*) _cert_name="c${_cert_name}" ;;
esac
# PEM content is already Base64 inside the -----BEGIN/END----- wrappers.
# The API expects the raw PEM as a JSON string, so newlines must be escaped as \n.
_cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')"
_key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')"
_debug2 _cert_name "$_cert_name"
_debug2 _domains_json "[$_domains_json]"
# Build JSON payload
_payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}"
# Generate BCE v1 authorization header (query string included in canonical request)
_cdn_path="/v2/domain/certificate"
_cdn_query="action=put"
_ts="$(_utc_date | sed 's/ /T/')Z"
_content_type="application/json; charset=utf-8"
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then
_err "Failed to sign request"
return 1
fi
_H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT"
_H2="x-bce-date: $_ts"
_H3="x-bce-content-sha256: $_payload_hash"
_H4="Host: $BAIDU_CDN_HOST"
_H5=""
_url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}"
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
if [ "$?" != "0" ]; then
_err "Failed to call Baidu Cloud CDN API"
return 1
fi
_debug2 response "$response"
if _contains "$response" "\"certId\""; then
_info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN"
return 0
fi
_err "Failed to deploy certificate to Baidu Cloud CDN: $response"
return 1
}
# BCE v1 signing with canonical query string support.
# The CDN endpoint uses ?action=put so it must be included in the canonical request.
_baidu_cdn_bce_auth() {
_method="$1"
_uri="$2"
_query="$3"
_host="$4"
_ts="$5"
_expire="$6"
_ct="$7"
_payload_hash="$8"
_BAIDU_CDN_BCE_AUTH_RESULT=""
_auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}"
_signed_headers="content-type;host;x-bce-content-sha256;x-bce-date"
_canonical_uri="$(_baidu_cdn_bce_encode_path "$_uri")"
_host_e="$(printf "%s" "$_host" | _url_encode upper-hex)"
_date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)"
_ct_e="$(printf "%s" "$_ct" | _url_encode upper-hex)"
_hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)"
_canonical_headers="content-type:${_ct_e}
host:${_host_e}
x-bce-content-sha256:${_hash_e}
x-bce-date:${_date_e}"
_canonical_request="${_method}
${_canonical_uri}
${_query}
${_canonical_headers}"
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
_signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
_signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
_BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
}
_baidu_cdn_load_credentials() {
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")"
Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")"
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
_err "Baidu_AK and Baidu_SK are required"
return 1
fi
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
return 0
}
_baidu_cdn_bce_encode_path() {
_p="$1"
_out=""
if [ "${_p#"/"}" != "$_p" ]; then
_out="/"
fi
_rest="${_p#/}"
while [ -n "$_rest" ]; do
_seg="${_rest%%/*}"
if [ "$_seg" ]; then
if [ -z "$_out" ] || [ "$_out" = "/" ]; then
_out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)"
else
_out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)"
fi
fi
if [ "${_rest#*/}" = "$_rest" ]; then
break
fi
_rest="${_rest#*/}"
done
if [ -z "$_out" ]; then
_out="/"
fi
printf "%s" "$_out"
}
_baidu_cdn_trim_ws() {
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
}
_baidu_cdn_json_escape() {
_s="$1"
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
printf "%s" "$_s" |
sed 's/\\/\\\\/g; s/ /\\t/g' |
_baidu_cdn_json_encode
}
_baidu_cdn_json_encode() {
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
}
_baidu_cdn_hmac_sha256_hexkey() {
_key_hex="$1"
_msg="$2"
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
}
-394
View File
@@ -1,394 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
#
# acme.sh deploy hook: BytePlus Application Load Balancer (ALB)
# https://github.com/acmesh-official/acme.sh/wiki/deployhooks
#
# Deploys SSL/TLS certificates issued by acme.sh to BytePlus ALB.
# Supports automatic renewal with zero-downtime certificate rotation
# for certificates that have already been uploaded and have a saved
# BytePlus CertificateId.
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ FIRST TIME (new domain) │
# │ 1. acme.sh --issue -d example.com -w /var/www/html/ │
# │ 2. Upload/import the certificate to BytePlus ALB manually │
# │ 3. Save/configure the existing CertificateId for this hook │
# │ 4. Manually assign cert to ALB Listener (one-time only) │
# │ │
# │ RENEWAL (fully automatic after CertificateId is configured) │
# │ acme.sh cron triggers renew → deploy hook runs automatically │
# │ → ReplaceCertificate (UpdateMode=new) — single API call │
# │ → All attached listeners updated, old cert auto-deleted │
# └─────────────────────────────────────────────────────────────────────┘
#
# Required environment variables:
# export BYTEPLUS_ACCESS_KEY="AKAPxxxxxxxxxx"
# export BYTEPLUS_SECRET_KEY="your-secret-key"
#
# Optional environment variables:
# export BYTEPLUS_REGION="ap-southeast-3" # default: ap-southeast-3
# export BYTEPLUS_HOST="alb.ap-southeast-3.byteplusapi.com" # custom API host
# export BYTEPLUS_PROJECT_NAME="live" # default: "default" project
# export BYTEPLUS_CERT_NAME="" # default: acme-{domain}-{YYYYMMDD-HHMM}
# export BYTEPLUS_CERT_DESCRIPTION="" # default: empty
# export BYTEPLUS_DELETE_OLD_CERT="true" # default: true — auto-delete after replace
#
# API notes:
# - All BytePlus ALB APIs use GET with query string parameters
# - Request signing: HMAC-SHA256 with signed headers host;x-date
# - PublicKey/PrivateKey are URL-encoded (RFC 3986) in query string
# - ReplaceCertificate with UpdateMode=new uploads + replaces in 1 call
#
# Dependencies: curl, openssl, awk (standard on most Linux)
#
# Docs:
# Signing — https://docs.byteplus.com/en/docs/byteplus-platform/reference-how-to-calculate-a-signature
# ALB API — https://docs.byteplus.com/en/docs/byteplus-alb
# ══════════════════════════════════════════════════════════════════════════════
# Constants
# ══════════════════════════════════════════════════════════════════════════════
# SHA-256 hash of empty string (used for GET requests with no body)
_BYTEPLUS_EMPTY_HASH="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# ══════════════════════════════════════════════════════════════════════════════
# Main deploy function — called by acme.sh
# ══════════════════════════════════════════════════════════════════════════════
byteplus_alb_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# ── 1. Load & validate credentials ──────────────────────────────────────────
# Preserve environment values before _getdeployconf (which may reset them)
_env_project_name="${BYTEPLUS_PROJECT_NAME:-}"
_env_delete_old="${BYTEPLUS_DELETE_OLD_CERT:-}"
_getdeployconf BYTEPLUS_ACCESS_KEY
_getdeployconf BYTEPLUS_SECRET_KEY
_getdeployconf BYTEPLUS_REGION
_getdeployconf BYTEPLUS_HOST
_getdeployconf BYTEPLUS_PROJECT_NAME
_getdeployconf BYTEPLUS_DELETE_OLD_CERT
_getdeployconf BYTEPLUS_CERT_NAME
# Restore from environment if _getdeployconf cleared them
if [ -z "$BYTEPLUS_PROJECT_NAME" ] && [ -n "$_env_project_name" ]; then
_debug "Restoring BYTEPLUS_PROJECT_NAME from environment"
BYTEPLUS_PROJECT_NAME="$_env_project_name"
fi
if [ -z "$BYTEPLUS_DELETE_OLD_CERT" ] && [ -n "$_env_delete_old" ]; then
BYTEPLUS_DELETE_OLD_CERT="$_env_delete_old"
fi
# Validate required credentials
if [ -z "$BYTEPLUS_ACCESS_KEY" ]; then
_err "BYTEPLUS_ACCESS_KEY is not set."
_err "Please run: export BYTEPLUS_ACCESS_KEY=\"your-access-key\""
return 1
fi
if [ -z "$BYTEPLUS_SECRET_KEY" ]; then
_err "BYTEPLUS_SECRET_KEY is not set."
_err "Please run: export BYTEPLUS_SECRET_KEY=\"your-secret-key\""
return 1
fi
# Save credentials for future runs
_savedeployconf BYTEPLUS_ACCESS_KEY "$BYTEPLUS_ACCESS_KEY"
_savedeployconf BYTEPLUS_SECRET_KEY "$BYTEPLUS_SECRET_KEY"
# Region (default: ap-southeast-3)
BYTEPLUS_REGION="${BYTEPLUS_REGION:-ap-southeast-3}"
_savedeployconf BYTEPLUS_REGION "$BYTEPLUS_REGION"
# Project name
if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then
_savedeployconf BYTEPLUS_PROJECT_NAME "$BYTEPLUS_PROJECT_NAME"
_info "Using project: $BYTEPLUS_PROJECT_NAME"
else
_info "WARNING: BYTEPLUS_PROJECT_NAME is not set. Cert will go to 'default' project."
fi
# Delete old cert toggle (default: true)
BYTEPLUS_DELETE_OLD_CERT="${BYTEPLUS_DELETE_OLD_CERT:-true}"
_savedeployconf BYTEPLUS_DELETE_OLD_CERT "$BYTEPLUS_DELETE_OLD_CERT"
# API host — custom override or auto-build from region
if [ -n "$BYTEPLUS_HOST" ]; then
_BYTEPLUS_HOST="$BYTEPLUS_HOST"
_savedeployconf BYTEPLUS_HOST "$BYTEPLUS_HOST"
else
_BYTEPLUS_HOST="alb.${BYTEPLUS_REGION}.byteplusapi.com"
fi
_info "Using API host: $_BYTEPLUS_HOST"
_BYTEPLUS_SERVICE="alb"
# ── 2. Build certificate name ────────────────────────────────────────────────
_date_tag=$(date -u +%Y%m%d-%H%M)
# Replace wildcard * and dots for a valid cert name
_safe_domain=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./-/g')
# Safe identifier version for deployconf keys: map all non [A-Za-z0-9_] to _
_conf_key=$(echo "$_cdomain" | sed 's/^\*\.//' | sed 's/[^A-Za-z0-9_]/_/g')
if [ -z "$BYTEPLUS_CERT_NAME" ]; then
BYTEPLUS_CERT_NAME="acme-${_safe_domain}-${_date_tag}"
fi
# Enforce BytePlus naming rules: start with letter, max 128 chars
BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^A-Za-z0-9._-]/-/g')
case "$BYTEPLUS_CERT_NAME" in
[A-Za-z]*) ;;
*)
BYTEPLUS_CERT_NAME="a$BYTEPLUS_CERT_NAME"
;;
esac
BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | cut -c1-128)
_info "Certificate name: $BYTEPLUS_CERT_NAME"
# ── 3. Read cert and key ─────────────────────────────────────────────────────
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
_err "Failed to read certificate or key file."
return 1
fi
# ── 4. Deploy: first-time upload or renewal replace ─────────────────────────
_getdeployconf "BYTEPLUS_CERT_ID_${_conf_key}"
_old_cert_id=$(eval echo "\$BYTEPLUS_CERT_ID_${_conf_key}")
if [ -z "$_old_cert_id" ]; then
_byteplus_first_time_deploy
else
_byteplus_renewal_deploy
fi
# Check if deploy step set _new_cert_id
if [ -z "$_new_cert_id" ]; then
return 1
fi
# ── 5. Save new CertificateId for next renewal ───────────────────────────────
_savedeployconf "BYTEPLUS_CERT_ID_${_conf_key}" "$_new_cert_id"
_info "Saved CertificateId '$_new_cert_id' for domain '$_cdomain'."
return 0
}
# ══════════════════════════════════════════════════════════════════════════════
# Deploy: First time — UploadCertificate
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_first_time_deploy() {
_info "No previous CertificateId found."
_err "Refusing to upload certificate material because this hook passes PublicKey/PrivateKey as request parameters."
_err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings."
_err "Please upload the certificate to BytePlus manually for the initial deployment, set BYTEPLUS_CERT_ID_${_conf_key} to that CertificateId, and rerun."
_err "This hook stores CertificateId values per domain using deployconf, so the variable name must include the current domain-specific suffix."
_err "This hook must be updated to send PublicKey and PrivateKey in a POST body before automatic first-time upload can be enabled safely."
return 1
}
# ══════════════════════════════════════════════════════════════════════════════
# Deploy: Renewal — ReplaceCertificate (UpdateMode=new)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_renewal_deploy() {
_info "Replacing old certificate '$_old_cert_id' (UpdateMode=new)..."
_err "Refusing to replace certificate material because this hook passes PublicKey/PrivateKey as request parameters."
_err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings."
_err "Please replace the certificate in BytePlus manually for renewal until this hook is updated to send PublicKey and PrivateKey in a POST body safely."
return 1
}
# ══════════════════════════════════════════════════════════════════════════════
# Delete old certificate (with retry)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_delete_old_cert() {
_del_cert_id="$1"
_info "Waiting 5s for cert status to settle..."
_sleep 5
_info "Deleting old certificate '$_del_cert_id'..."
_del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}")
if echo "$_del_response" | grep -q '"Error"'; then
_info "Delete failed, retrying in 10s..."
_sleep 10
_del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}")
if echo "$_del_response" | grep -q '"Error"'; then
_info "Warning: Could not delete old certificate '$_del_cert_id'."
_info "Error: $(_byteplus_extract_error "$_del_response")"
_info "Please remove it manually from BytePlus Console."
else
_info "Old certificate '$_del_cert_id' deleted (retry succeeded)."
fi
else
_info "Old certificate '$_del_cert_id' deleted."
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# JSON response helpers
# ══════════════════════════════════════════════════════════════════════════════
# Extract CertificateId from API response JSON
_byteplus_extract_cert_id() {
echo "$1" | _egrep_o '"CertificateId"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"'
}
# Extract error message from API response JSON
_byteplus_extract_error() {
_code=$(echo "$1" | _egrep_o '"Code"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"')
_msg=$(echo "$1" | _egrep_o '"Message"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"')
if [ -n "$_code" ]; then
printf '%s — %s' "$_code" "$_msg"
else
printf '%s' "$1"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# BytePlus ALB API caller
# ══════════════════════════════════════════════════════════════════════════════
# Usage: _byteplus_alb_api ACTION [param1=val1] [param2=val2] ...
# All parameters sent via GET query string. Signing: HMAC-SHA256, host;x-date.
_byteplus_alb_api() {
_action="$1"
shift
# Build query string — all params go in URL
_query_params="Action=${_action}&Version=2020-04-01"
for _param in "$@"; do
_pname="${_param%%=*}"
_pval="${_param#*=}"
_query_params="${_query_params}&${_pname}=$(_byteplus_urlencode "$_pval")"
done
# Timestamps
_x_date=$(date -u +%Y%m%dT%H%M%SZ)
_date_only=$(date -u +%Y%m%d)
# Sort query params for canonical request
_sorted_query=$(echo "$_query_params" | tr '&' '\n' | LC_ALL=C sort | tr '\n' '&' | sed 's/&$//')
# Canonical headers — only host and x-date
_canonical_headers="host:${_BYTEPLUS_HOST}
x-date:${_x_date}
"
_signed_headers="host;x-date"
# Canonical request
_canonical_request="GET
/
${_sorted_query}
${_canonical_headers}
${_signed_headers}
${_BYTEPLUS_EMPTY_HASH}"
# Do not log _canonical_request because the query string may contain
# URL-encoded certificate or private key material.
# Hash of canonical request
# _digest is provided by acme.sh and works across OpenSSL versions.
_cr_hash=$(printf '%s' "$_canonical_request" | _digest sha256 hex)
# Credential scope
_credential_scope="${_date_only}/${BYTEPLUS_REGION}/${_BYTEPLUS_SERVICE}/request"
# String to sign
_string_to_sign="HMAC-SHA256
${_x_date}
${_credential_scope}
${_cr_hash}"
_debug2 _string_to_sign "$_string_to_sign"
# Signing key derivation (HMAC chain)
# _hmac <algo> <hex-key> reads data from stdin and returns a hex digest.
# acme.sh's _hmac abstracts away OpenSSL version differences, so this works
# on both modern (-mac HMAC -macopt hexkey:) and older (-hmac) OpenSSL builds.
#
# The first step seeds the chain from the raw secret key, so we convert it
# to hex first with _hex_dump (also an acme.sh built-in).
_secret_hex=$(printf '%s' "$BYTEPLUS_SECRET_KEY" | _hex_dump | tr -d ' \n')
_k_date=$(printf '%s' "$_date_only" | _hmac sha256 "$_secret_hex" hex)
_k_region=$(printf '%s' "$BYTEPLUS_REGION" | _hmac sha256 "$_k_date" hex)
_k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | _hmac sha256 "$_k_region" hex)
_k_signing=$(printf '%s' "request" | _hmac sha256 "$_k_service" hex)
# Final signature
_signature=$(printf '%s' "$_string_to_sign" | _hmac sha256 "$_k_signing" hex)
# Authorization header
_auth="HMAC-SHA256 Credential=${BYTEPLUS_ACCESS_KEY}/${_credential_scope}, SignedHeaders=${_signed_headers}, Signature=${_signature}"
_secure_debug2 _auth "$_auth"
# Send request parameters in the POST body instead of the URL query string.
# This avoids exposing sensitive or large values in debug-logged URLs and
# reduces the risk of exceeding URL length limits.
_url="https://${_BYTEPLUS_HOST}/"
_body="$_sorted_query"
_saved_H1="${_H1:-}"
_saved_H2="${_H2:-}"
_saved_H3="${_H3:-}"
_saved_H4="${_H4:-}"
_saved_H5="${_H5:-}"
_H1="Authorization: ${_auth}"
_H2="X-Date: ${_x_date}"
_H3="Host: ${_BYTEPLUS_HOST}"
_H4="Content-Type: application/x-www-form-urlencoded"
_H5=""
_response="$(_post "$_body" "$_url" "" "POST")"
_request_ret="$?"
_H1="$_saved_H1"
_H2="$_saved_H2"
_H3="$_saved_H3"
_H4="$_saved_H4"
_H5="$_saved_H5"
if [ "$_request_ret" != "0" ]; then
_err "byteplus_alb_api request failed for [$_action]"
return 1
fi
_debug2 "_byteplus_alb_api response [$_action]" "$_response"
printf '%s' "$_response"
}
# ══════════════════════════════════════════════════════════════════════════════
# URL encode (RFC 3986)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_urlencode() {
printf '%s' "$1" | _url_encode
}
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificate to CacheFly
# https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
# This deployment required following variables
# export CACHEFLY_TOKEN="Your CacheFly API Token"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
CACHEFLY_API_BASE="https://api.cachefly.com/api/2.5"
cachefly_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$CACHEFLY_TOKEN" ]; then
_err "CACHEFLY_TOKEN is not defined."
return 1
else
_savedomainconf CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
fi
_info "Deploying certificate to CacheFly..."
## upload certificate
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
_request_body="{\"certificate\":\"$string_fullchain\",\"certificateKey\":\"$string_key\"}"
_debug _request_body "$_request_body"
_debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
export _H1="Authorization: Bearer $CACHEFLY_TOKEN"
_response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json")
if _contains "$_response" "message"; then
_err "Error in deploying $_cdomain certificate to CacheFly."
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Domain $_cdomain certificate successfully deployed to CacheFly."
return 0
}
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env sh
# Here is the script to deploy the cert to your CleverReach Account using the CleverReach REST API.
# Your OAuth needs the right scope, please contact CleverReach support for that.
#
# Written by Jan-Philipp Benecke <github@bnck.me>
# Public domain, 2020
#
# Following environment variables must be set:
#
#export DEPLOY_CLEVERREACH_CLIENT_ID=myid
#export DEPLOY_CLEVERREACH_CLIENT_SECRET=mysecret
cleverreach_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_rest_endpoint="https://rest.cleverreach.com"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_CLEVERREACH_CLIENT_ID
_getdeployconf DEPLOY_CLEVERREACH_CLIENT_SECRET
_getdeployconf DEPLOY_CLEVERREACH_SUBCLIENT_ID
if [ -z "${DEPLOY_CLEVERREACH_CLIENT_ID}" ]; then
_err "CleverReach Client ID is not found, please define DEPLOY_CLEVERREACH_CLIENT_ID."
return 1
fi
if [ -z "${DEPLOY_CLEVERREACH_CLIENT_SECRET}" ]; then
_err "CleverReach client secret is not found, please define DEPLOY_CLEVERREACH_CLIENT_SECRET."
return 1
fi
_savedeployconf DEPLOY_CLEVERREACH_CLIENT_ID "${DEPLOY_CLEVERREACH_CLIENT_ID}"
_savedeployconf DEPLOY_CLEVERREACH_CLIENT_SECRET "${DEPLOY_CLEVERREACH_CLIENT_SECRET}"
_savedeployconf DEPLOY_CLEVERREACH_SUBCLIENT_ID "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}"
_info "Obtaining a CleverReach access token"
_data="{\"grant_type\": \"client_credentials\", \"client_id\": \"${DEPLOY_CLEVERREACH_CLIENT_ID}\", \"client_secret\": \"${DEPLOY_CLEVERREACH_CLIENT_SECRET}\"}"
_auth_result="$(_post "$_data" "$_rest_endpoint/oauth/token.php" "" "POST" "application/json")"
_debug _data "$_data"
_debug _auth_result "$_auth_result"
_regex=".*\"access_token\":\"\([-._0-9A-Za-z]*\)\".*$"
_debug _regex "$_regex"
_access_token=$(echo "$_auth_result" | _json_decode | sed -n "s/$_regex/\1/p")
_debug _subclient "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}"
if [ -n "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" ]; then
_info "Obtaining token for sub-client ${DEPLOY_CLEVERREACH_SUBCLIENT_ID}"
export _H1="Authorization: Bearer ${_access_token}"
_subclient_token_result="$(_get "$_rest_endpoint/v3/clients/$DEPLOY_CLEVERREACH_SUBCLIENT_ID/token")"
_access_token=$(echo "$_subclient_token_result" | sed -n "s/\"//p")
_debug _subclient_token_result "$_access_token"
_info "Destroying parent token at CleverReach, as it not needed anymore"
_destroy_result="$(_post "" "$_rest_endpoint/v3/oauth/token.json" "" "DELETE" "application/json")"
_debug _destroy_result "$_destroy_result"
fi
_info "Uploading certificate and key to CleverReach"
_certData="{\"cert\":\"$(_json_encode <"$_cfullchain")\", \"key\":\"$(_json_encode <"$_ckey")\"}"
export _H1="Authorization: Bearer ${_access_token}"
_add_cert_result="$(_post "$_certData" "$_rest_endpoint/v3/ssl" "" "POST" "application/json")"
if [ -z "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" ]; then
_info "Destroying token at CleverReach, as it not needed anymore"
_destroy_result="$(_post "" "$_rest_endpoint/v3/oauth/token.json" "" "DELETE" "application/json")"
_debug _destroy_result "$_destroy_result"
fi
if ! echo "$_add_cert_result" | grep '"error":' >/dev/null; then
_info "Uploaded certificate successfully"
return 0
else
_debug _add_cert_result "$_add_cert_result"
_err "Unable to update certificate"
return 1
fi
}
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to hashicorp consul using curl
# (https://www.consul.io/)
#
# it requires following environment variables:
#
# CONSUL_PREFIX - this contains the prefix path in consul
# CONSUL_HTTP_ADDR - consul requires this to find your consul server
#
# additionally, you need to ensure that CONSUL_HTTP_TOKEN is available
# to access the consul server
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
consul_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# validate required env vars
_getdeployconf CONSUL_PREFIX
if [ -z "$CONSUL_PREFIX" ]; then
_err "CONSUL_PREFIX needs to be defined (contains prefix path in vault)"
return 1
fi
_savedeployconf CONSUL_PREFIX "$CONSUL_PREFIX"
_getdeployconf CONSUL_HTTP_ADDR
if [ -z "$CONSUL_HTTP_ADDR" ]; then
_err "CONSUL_HTTP_ADDR needs to be defined (contains consul connection address)"
return 1
fi
_savedeployconf CONSUL_HTTP_ADDR "$CONSUL_HTTP_ADDR"
CONSUL_CMD=$(command -v consul)
# force CLI, but the binary does not exist => error
if [ -n "$USE_CLI" ] && [ -z "$CONSUL_CMD" ]; then
_err "Cannot find the consul binary!"
return 1
fi
# use the CLI first
if [ -n "$USE_CLI" ] || [ -n "$CONSUL_CMD" ]; then
_info "Found consul binary, deploying with CLI"
consul_deploy_cli "$CONSUL_CMD" "$CONSUL_PREFIX"
else
_info "Did not find consul binary, deploying with API"
consul_deploy_api "$CONSUL_HTTP_ADDR" "$CONSUL_PREFIX" "$CONSUL_HTTP_TOKEN"
fi
}
consul_deploy_api() {
CONSUL_HTTP_ADDR="$1"
CONSUL_PREFIX="$2"
CONSUL_HTTP_TOKEN="$3"
URL="$CONSUL_HTTP_ADDR/v1/kv/$CONSUL_PREFIX"
export _H1="X-Consul-Token: $CONSUL_HTTP_TOKEN"
if [ -n "$FABIO" ]; then
_post "$(cat "$_cfullchain")" "$URL/${_cdomain}-cert.pem" '' "PUT" || return 1
_post "$(cat "$_ckey")" "$URL/${_cdomain}-key.pem" '' "PUT" || return 1
else
_post "$(cat "$_ccert")" "$URL/${_cdomain}/cert.pem" '' "PUT" || return 1
_post "$(cat "$_ckey")" "$URL/${_cdomain}/cert.key" '' "PUT" || return 1
_post "$(cat "$_cca")" "$URL/${_cdomain}/chain.pem" '' "PUT" || return 1
_post "$(cat "$_cfullchain")" "$URL/${_cdomain}/fullchain.pem" '' "PUT" || return 1
fi
}
consul_deploy_cli() {
CONSUL_CMD="$1"
CONSUL_PREFIX="$2"
if [ -n "$FABIO" ]; then
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}-cert.pem" @"$_cfullchain" || return 1
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}-key.pem" @"$_ckey" || return 1
else
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
$CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
fi
}
-224
View File
@@ -1,224 +0,0 @@
#!/usr/bin/env sh
# Here is the script to deploy the cert to your cpanel using the cpanel API.
# Uses command line uapi. --user option is needed only if run as root.
# Returns 0 when success.
#
# Configure DEPLOY_CPANEL_AUTO_<...> options to enable or restrict automatic
# detection of deployment targets through UAPI (if not set, defaults below are used.)
# - ENABLED : 'true' for multi-site / wildcard capability; otherwise single-site mode.
# - NOMATCH : 'true' to allow deployment to sites that do not match the certificate.
# - INCLUDE : Comma-separated list - sites must match this field.
# - EXCLUDE : Comma-separated list - sites must NOT match this field.
# INCLUDE/EXCLUDE both support non-lexical, glob-style matches using '*'
#
# Please note that I am no longer using Github. If you want to report an issue
# or contact me, visit https://forum.webseodesigners.com/web-design-seo-and-hosting-f16/
#
# Written by Santeri Kannisto <santeri.kannisto@webseodesigners.com>
# Public domain, 2017-2018
#
# export DEPLOY_CPANEL_USER=myusername
# export DEPLOY_CPANEL_AUTO_ENABLED='true'
# export DEPLOY_CPANEL_AUTO_NOMATCH='false'
# export DEPLOY_CPANEL_AUTO_INCLUDE='*'
# export DEPLOY_CPANEL_AUTO_EXCLUDE=''
######## Public functions #####################
#domain keyfile certfile cafile fullchain
cpanel_uapi_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
# re-declare vars inherited from acme.sh but not passed to make ShellCheck happy
: "${Le_Alt:=""}"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists uapi; then
_err "The command uapi is not found."
return 1
fi
# declare useful constants
uapi_error_response='status: 0'
# read cert and key files and urlencode both
_cert=$(_url_encode <"$_ccert")
# with --signcsr the private key was never handed to acme.sh, so the key
# file does not exist; skip it instead of spilling a shell redirection
# error on every renewal (cPanel keeps using the already-installed key)
if [ -f "$_ckey" ]; then
_key=$(_url_encode <"$_ckey")
else
_debug "Key file $_ckey does not exist (csr mode), not sending a key."
_key=""
fi
_debug2 _cert "$_cert"
_debug2 _key "$_key"
if [ "$(id -u)" = 0 ]; then
_getdeployconf DEPLOY_CPANEL_USER
# fallback to _readdomainconf for old installs
if [ -z "${DEPLOY_CPANEL_USER:=$(_readdomainconf DEPLOY_CPANEL_USER)}" ]; then
_err "It seems that you are root, please define the target user name: export DEPLOY_CPANEL_USER=username"
return 1
fi
_debug DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
_savedeployconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
_uapi_user="$DEPLOY_CPANEL_USER"
fi
# Load all AUTO envars and set defaults - see above for usage
__cpanel_initautoparam ENABLED 'true'
__cpanel_initautoparam NOMATCH 'false'
__cpanel_initautoparam INCLUDE '*'
__cpanel_initautoparam EXCLUDE ''
# Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
else
_response=$(uapi DomainInfo list_domains)
fi
# exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:"
_err "\n$_response"
return 1
fi
# parse response to create site list
sitelist=$(__cpanel_parse_response "$_response")
_debug "UAPI sites found: $sitelist"
# filter sitelist using configured domains
# skip if NOMATCH is "true"
if [ "$DEPLOY_CPANEL_AUTO_NOMATCH" = "true" ]; then
_debug "DEPLOY_CPANEL_AUTO_NOMATCH is true"
_info "UAPI nomatch mode is enabled - Will not validate sites are valid for the certificate"
else
_debug "DEPLOY_CPANEL_AUTO_NOMATCH is false"
d="$(echo "${Le_Alt}," | sed -e "s/^$_cdomain,//" -e "s/,$_cdomain,/,/")"
d="$(echo "$_cdomain,$d" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\[\^\.\]\*/g')"
sitelist="$(echo "$sitelist" | grep -ix "$d")"
_debug2 "Matched UAPI sites: $sitelist"
fi
# filter sites that do not match $DEPLOY_CPANEL_AUTO_INCLUDE
_info "Applying sitelist filter DEPLOY_CPANEL_AUTO_INCLUDE: $DEPLOY_CPANEL_AUTO_INCLUDE"
sitelist="$(echo "$sitelist" | grep -ix "$(echo "$DEPLOY_CPANEL_AUTO_INCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")"
_debug2 "Remaining sites: $sitelist"
# filter sites that match $DEPLOY_CPANEL_AUTO_EXCLUDE
_info "Applying sitelist filter DEPLOY_CPANEL_AUTO_EXCLUDE: $DEPLOY_CPANEL_AUTO_EXCLUDE"
sitelist="$(echo "$sitelist" | grep -vix "$(echo "$DEPLOY_CPANEL_AUTO_EXCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")"
_debug2 "Remaining sites: $sitelist"
# counter for success / failure check
successes=0
if [ -n "$sitelist" ]; then
sitetotal="$(echo "$sitelist" | wc -l)"
_debug "$sitetotal sites to deploy"
else
sitetotal=0
_debug "No sites to deploy"
fi
# for each site: call uapi to publish cert and log result. Only return failure if all fail
for site in $sitelist; do
# call uapi to publish cert, check response for errors and log them.
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$site" cert="$_cert" key="$_key")
else
_response=$(uapi SSL install_ssl domain="$site" cert="$_cert" key="$_key")
fi
if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate to $site:"
_err "$_response"
else
successes=$((successes + 1))
_debug "$_response"
_info "Succcessfully deployed to $site"
fi
done
# Raise error if all updates fail
if [ "$sitetotal" -gt 0 ] && [ "$successes" -eq 0 ]; then
_err "Could not deploy to any of $sitetotal sites via UAPI"
_debug "successes: $successes, sitetotal: $sitetotal"
return 1
fi
_info "Successfully deployed certificate to $successes of $sitetotal sites via UAPI"
return 0
else
# "classic" mode - will only try to deploy to the primary domain; will not check UAPI first
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
else
_response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
fi
if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate:"
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Certificate successfully deployed"
return 0
fi
}
######## Private functions #####################
# Internal utility to process YML from UAPI - looks at main_domain, sub_domains, addon domains and parked domains
#[response]
__cpanel_parse_response() {
if [ $# -gt 0 ]; then resp="$*"; else resp="$(cat)"; fi
echo "$resp" |
sed -En \
-e 's/\r$//' \
-e 's/^( *)([_.[:alnum:]]+) *: *(.*)/\1,\2,\3/p' \
-e 's/^( *)- (.*)/\1,-,\2/p' |
awk -F, '{
level = length($1)/2;
section[level] = $2;
for (i in section) {if (i > level) {delete section[i]}}
if (length($3) > 0) {
prefix="";
for (i=0; i < level; i++)
{ prefix = (prefix)(section[i])("/") }
printf("%s%s=%s\n", prefix, $2, $3);
}
}' |
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
}
# Load parameter by prefix+name - fallback to default if not set, and save to config
#pname pdefault
__cpanel_initautoparam() {
pname="$1"
pdefault="$2"
pkey="DEPLOY_CPANEL_AUTO_$pname"
_getdeployconf "$pkey"
[ -n "$(eval echo "\"\$$pkey\"")" ] || eval "$pkey=\"$pdefault\""
_debug2 "$pkey" "$(eval echo "\"\$$pkey\"")"
_savedeployconf "$pkey" "$(eval echo "\"\$$pkey\"")"
}
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificate to DirectAdmin
# https://docs.directadmin.com/directadmin/customizing-workflow/api-all-about.html#creating-a-login-key
# https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs
# This deployment required following variables
# export DirectAdmin_SCHEME="https" # Optional, https or http, defaults to https
# export DirectAdmin_ENDPOINT="example.com:2222"
# export DirectAdmin_USERNAME="Your DirectAdmin Username"
# export DirectAdmin_KEY="Your DirectAdmin Login Key or Password"
# export DirectAdmin_MAIN_DOMAIN="Your DirectAdmin Main Domain, NOT Subdomain"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
directadmin_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$DirectAdmin_ENDPOINT" ]; then
_err "DirectAdmin_ENDPOINT is not defined."
return 1
else
_savedomainconf DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
fi
if [ -z "$DirectAdmin_USERNAME" ]; then
_err "DirectAdmin_USERNAME is not defined."
return 1
else
_savedomainconf DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
fi
if [ -z "$DirectAdmin_KEY" ]; then
_err "DirectAdmin_KEY is not defined."
return 1
else
_savedomainconf DirectAdmin_KEY "$DirectAdmin_KEY"
fi
if [ -z "$DirectAdmin_MAIN_DOMAIN" ]; then
_err "DirectAdmin_MAIN_DOMAIN is not defined."
return 1
else
_savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
fi
# Optional SCHEME
_getdeployconf DirectAdmin_SCHEME
# set default values for DirectAdmin_SCHEME
[ -n "${DirectAdmin_SCHEME}" ] || DirectAdmin_SCHEME="https"
_info "Deploying certificate to DirectAdmin..."
# upload certificate
string_cfullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
_request_body="{\"domain\":\"$DirectAdmin_MAIN_DOMAIN\",\"action\":\"save\",\"type\":\"paste\",\"certificate\":\"$string_key\n$string_cfullchain\n\"}"
_debug _request_body "$_request_body"
_debug DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
_debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
_debug DirectAdmin_KEY "$DirectAdmin_KEY"
_debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
_response=$(_post "$_request_body" "$DirectAdmin_SCHEME://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json")
if _contains "$_response" "error=1"; then
_err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
_err "$_response"
return 1
fi
_info "$_response"
_info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
return 0
}
-352
View File
@@ -1,352 +0,0 @@
#!/usr/bin/env sh
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
#DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="service nginx force-reload"
_DEPLOY_DOCKER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/deploy-to-docker-containers"
_DOCKER_HOST_DEFAULT="/var/run/docker.sock"
docker_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL
_debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL"
if [ -z "$DEPLOY_DOCKER_CONTAINER_LABEL" ]; then
_err "The DEPLOY_DOCKER_CONTAINER_LABEL variable is not defined, we use this label to find the container."
_err "See: $_DEPLOY_DOCKER_WIKI"
fi
_savedeployconf DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL"
if [ "$DOCKER_HOST" ]; then
_saveaccountconf DOCKER_HOST "$DOCKER_HOST"
fi
if _exists docker && docker version | grep -i docker >/dev/null; then
_info "Using docker command"
export _USE_DOCKER_COMMAND=1
else
export _USE_DOCKER_COMMAND=
fi
export _USE_UNIX_SOCKET=
if [ -z "$_USE_DOCKER_COMMAND" ]; then
export _USE_REST=
if [ "$DOCKER_HOST" ]; then
_debug "Try use docker host: $DOCKER_HOST"
export _USE_REST=1
else
export _DOCKER_SOCK="$_DOCKER_HOST_DEFAULT"
_debug "Try use $_DOCKER_SOCK"
if [ ! -e "$_DOCKER_SOCK" ] || [ ! -w "$_DOCKER_SOCK" ]; then
_err "$_DOCKER_SOCK is not available"
return 1
fi
export _USE_UNIX_SOCKET=1
if ! _exists "curl"; then
_err "Please install curl first."
_err "We need curl to work."
return 1
fi
if ! _check_curl_version; then
return 1
fi
fi
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_CA_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_CA_FILE "$DEPLOY_DOCKER_CONTAINER_CA_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_CA_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_CA_FILE "$DEPLOY_DOCKER_CONTAINER_CA_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD
_debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" "base64"
fi
_cid="$(_get_id "$DEPLOY_DOCKER_CONTAINER_LABEL")"
_info "Container id: $_cid"
if [ -z "$_cid" ]; then
_err "can not find container id"
return 1
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" ]; then
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
return 1
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change owner of key file in container"
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change mode of key file in container"
return 1
fi
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
if ! _docker_cp "$_cid" "$_ccert" "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"; then
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_CA_FILE" ]; then
if ! _docker_cp "$_cid" "$_cca" "$DEPLOY_DOCKER_CONTAINER_CA_FILE"; then
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" ]; then
if ! _docker_cp "$_cid" "$_cfullchain" "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"; then
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
if ! _docker_cp "$_cid" "$_cpfx" "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"; then
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
_info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then
return 1
fi
fi
return 0
}
#label
_get_id() {
_label="$1"
if [ "$_USE_DOCKER_COMMAND" ]; then
docker ps -f label="$_label" --format "{{.ID}}"
elif [ "$_USE_REST" ]; then
_err "Not implemented yet."
return 1
elif [ "$_USE_UNIX_SOCKET" ]; then
_req="{\"label\":[\"$_label\"]}"
_debug2 _req "$_req"
_req="$(printf "%s" "$_req" | _url_encode)"
_debug2 _req "$_req"
listjson="$(_curl_unix_sock "${_DOCKER_SOCK:-$_DOCKER_HOST_DEFAULT}" GET "/containers/json?filters=$_req")"
_debug2 "listjson" "$listjson"
echo "$listjson" | tr '{,' '\n' | grep -i '"id":' | _head_n 1 | cut -d '"' -f 4
else
_err "Not implemented yet."
return 1
fi
}
#id cmd
_docker_exec() {
_eargs="$*"
_debug2 "_docker_exec $_eargs"
_dcid="$1"
shift
if [ "$_USE_DOCKER_COMMAND" ]; then
docker exec -i "$_dcid" sh -c "$*"
elif [ "$_USE_REST" ]; then
_err "Not implemented yet."
return 1
elif [ "$_USE_UNIX_SOCKET" ]; then
_cmd="$*"
#_cmd="$(printf "%s" "$_cmd" | sed 's/ /","/g')"
_debug2 _cmd "$_cmd"
#create exec instance:
cjson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/containers/$_dcid/exec" "{\"Cmd\": [\"sh\", \"-c\", \"$_cmd\"]}")"
_debug2 cjson "$cjson"
execid="$(echo "$cjson" | cut -d '"' -f 4)"
_debug execid "$execid"
#Detach:true is required for podman's docker-compatible API: with
#Detach:false it streams the command output on the connection, so the
#non-empty response was misread as an error (issue #4977). The real
#result is checked via the exec inspect ExitCode below instead.
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
_debug2 ejson "$ejson"
_et=0
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
sleep 1
_et="$(_math "$_et" + 1)"
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
done
_debug2 ijson "$ijson"
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
_err "docker exec error: $ijson"
return 1
fi
else
_err "Not implemented yet."
return 1
fi
}
#id from to
_docker_cp() {
_dcid="$1"
_from="$2"
_to="$3"
_info "Copying file from $_from to $_to"
_dir="$(dirname "$_to")"
_debug2 _dir "$_dir"
if ! _docker_exec "$_dcid" mkdir -p "$_dir"; then
_err "Can not create dir: $_dir"
return 1
fi
if [ "$_USE_DOCKER_COMMAND" ]; then
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
_docker_exec "$_dcid" tee "$_to" <"$_from"
else
_docker_exec "$_dcid" tee "$_to" <"$_from" >/dev/null
fi
if [ "$?" = "0" ]; then
_info "Success"
return 0
else
_info "Error"
return 1
fi
elif [ "$_USE_REST" ]; then
_err "Not implemented yet."
return 1
elif [ "$_USE_UNIX_SOCKET" ]; then
_frompath="$_from"
if _startswith "$_frompath" '/'; then
_frompath="$(echo "$_from" | cut -b 2-)" #remove the first '/' char
fi
_debug2 "_frompath" "$_frompath"
_toname="$(basename "$_to")"
_debug2 "_toname" "$_toname"
_debug2 "_from" "$_from"
if ! tar --transform="s,$(printf "%s" "$_frompath" | tr '*' .),$_toname," -cz "$_from" 2>/dev/null | _curl_unix_sock "$_DOCKER_SOCK" PUT "/containers/$_dcid/archive?noOverwriteDirNonDir=1&path=$(printf "%s" "$_dir" | _url_encode)" '@-' "Content-Type: application/octet-stream"; then
_err "copy error"
return 1
fi
return 0
else
_err "Not implemented yet."
return 1
fi
}
#sock method endpoint data content-type
_curl_unix_sock() {
_socket="$1"
_method="$2"
_endpoint="$3"
_data="$4"
_ctype="$5"
if [ -z "$_ctype" ]; then
_ctype="Content-Type: application/json"
fi
_debug _data "$_data"
_debug2 "url" "http://localhost$_endpoint"
if [ "$_CURL_NO_HOST" ]; then
_cux_url="http:$_endpoint"
else
_cux_url="http://localhost$_endpoint"
fi
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
curl -vvv --silent --unix-socket "$_socket" -X "$_method" --data-binary "$_data" --header "$_ctype" "$_cux_url"
else
curl --silent --unix-socket "$_socket" -X "$_method" --data-binary "$_data" --header "$_ctype" "$_cux_url"
fi
}
_check_curl_version() {
_cversion="$(curl -V | grep '^curl ' | cut -d ' ' -f 2)"
_debug2 "_cversion" "$_cversion"
_major="$(_getfield "$_cversion" 1 '.')"
_debug2 "_major" "$_major"
_minor="$(_getfield "$_cversion" 2 '.')"
_debug2 "_minor" "$_minor"
if [ "$_major" -ge "8" ]; then
#ok
return 0
fi
if [ "$_major" = "7" ]; then
if [ "$_minor" -lt "40" ]; then
_err "curl v$_cversion doesn't support unit socket"
_err "Please upgrade to curl 7.40 or later."
return 1
fi
if [ "$_minor" -lt "50" ]; then
_debug "Use short host name"
export _CURL_NO_HOST=1
else
export _CURL_NO_HOST=
fi
return 0
else
_err "curl v$_cversion doesn't support unit socket"
_err "Please upgrade to curl 7.40 or later."
return 1
fi
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to dovecot server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
dovecot_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "Not implemented yet"
return 1
}
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to edgio using its API
# https://docs.edg.io/guides/v7/develop/rest_api/authentication
# https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
# This deployment required following variables
# export EDGIO_CLIENT_ID="Your Edgio Client ID"
# export EDGIO_CLIENT_SECRET="Your Edgio Client Secret"
# export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID"
# If have more than one Environment ID
# export EDGIO_ENVIRONMENT_ID="ENVIRONMENT_ID_1 ENVIRONMENT_ID_2"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
edgio_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$EDGIO_CLIENT_ID" ]; then
_err "EDGIO_CLIENT_ID is not defined."
return 1
else
_savedomainconf EDGIO_CLIENT_ID "$EDGIO_CLIENT_ID"
fi
if [ -z "$EDGIO_CLIENT_SECRET" ]; then
_err "EDGIO_CLIENT_SECRET is not defined."
return 1
else
_savedomainconf EDGIO_CLIENT_SECRET "$EDGIO_CLIENT_SECRET"
fi
if [ -z "$EDGIO_ENVIRONMENT_ID" ]; then
_err "EDGIO_ENVIRONMENT_ID is not defined."
return 1
else
_savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID"
fi
_info "Getting access token"
_data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config"
_debug Get_access_token_data "$_data"
_response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded")
_debug Get_access_token_response "$_response"
_access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"')
_debug _access_token "$_access_token"
if [ -z "$_access_token" ]; then
_err "Error in getting access token"
return 1
fi
_info "Uploading certificate"
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
for ENVIRONMENT_ID in $EDGIO_ENVIRONMENT_ID; do
_data="{\"environment_id\":\"$ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}"
_debug Upload_certificate_data "$_data"
_H1="Authorization: Bearer $_access_token"
_response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json")
if _contains "$_response" "message"; then
_err "Error in deploying $_cdomain certificate to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
_err "$_response"
return 1
fi
_debug Upload_certificate_response "$_response"
_info "Domain $_cdomain certificate successfully deployed to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
done
return 0
}
-113
View File
@@ -1,113 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to exim4 server.
#returns 0 means success, otherwise error.
#DEPLOY_EXIM4_CONF="/etc/exim/exim.conf"
#DEPLOY_EXIM4_RELOAD="service exim4 restart"
######## Public functions #####################
#domain keyfile certfile cafile fullchain
exim4_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_ssl_path="/etc/acme.sh/exim4"
if ! mkdir -p "$_ssl_path"; then
_err "Can not create folder:$_ssl_path"
return 1
fi
_info "Copying key and cert"
_real_key="$_ssl_path/exim4.key"
if ! cat "$_ckey" >"$_real_key"; then
_err "Error: write key file to: $_real_key"
return 1
fi
_real_fullchain="$_ssl_path/exim4.pem"
if ! cat "$_cfullchain" >"$_real_fullchain"; then
_err "Error: write key file to: $_real_fullchain"
return 1
fi
DEFAULT_EXIM4_RELOAD="service exim4 restart"
_reload="${DEPLOY_EXIM4_RELOAD:-$DEFAULT_EXIM4_RELOAD}"
if [ -z "$IS_RENEW" ]; then
DEFAULT_EXIM4_CONF="/etc/exim/exim.conf"
if [ ! -f "$DEFAULT_EXIM4_CONF" ]; then
DEFAULT_EXIM4_CONF="/etc/exim4/exim4.conf.template"
fi
_exim4_conf="${DEPLOY_EXIM4_CONF:-$DEFAULT_EXIM4_CONF}"
_debug _exim4_conf "$_exim4_conf"
if [ ! -f "$_exim4_conf" ]; then
if [ -z "$DEPLOY_EXIM4_CONF" ]; then
_err "exim4 conf is not found, please define DEPLOY_EXIM4_CONF"
return 1
else
_err "It seems that the specified exim4 conf is not valid, please check."
return 1
fi
fi
if [ ! -w "$_exim4_conf" ]; then
_err "The file $_exim4_conf is not writable, please change the permission."
return 1
fi
_backup_conf="$DOMAIN_BACKUP_PATH/exim4.conf.bak"
_info "Backup $_exim4_conf to $_backup_conf"
cp "$_exim4_conf" "$_backup_conf"
_info "Modify exim4 conf: $_exim4_conf"
if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" &&
_setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then
_info "Set config success!"
else
_err "Config exim4 server error, please report bug to us."
_info "Restoring exim4 conf"
if cat "$_backup_conf" >"$_exim4_conf"; then
_info "Restore conf success"
eval "$_reload"
else
_err "Oops, error restore exim4 conf, please report bug to us."
fi
return 1
fi
fi
_info "Run reload: $_reload"
if eval "$_reload"; then
_info "Reload success!"
if [ "$DEPLOY_EXIM4_CONF" ]; then
_savedomainconf DEPLOY_EXIM4_CONF "$DEPLOY_EXIM4_CONF"
else
_cleardomainconf DEPLOY_EXIM4_CONF
fi
if [ "$DEPLOY_EXIM4_RELOAD" ]; then
_savedomainconf DEPLOY_EXIM4_RELOAD "$DEPLOY_EXIM4_RELOAD"
else
_cleardomainconf DEPLOY_EXIM4_RELOAD
fi
return 0
else
_err "Reload error, restoring"
if cat "$_backup_conf" >"$_exim4_conf"; then
_info "Restore conf success"
eval "$_reload"
else
_err "Oops, error restore exim4 conf, please report bug to us."
fi
return 1
fi
}
-175
View File
@@ -1,175 +0,0 @@
#!/usr/bin/env sh
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
#
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
#
# REQUIRED:
# export FGT_HOST="fortigate_hostname-or-ip"
# export FGT_TOKEN="fortigate_api_token"
#
# OPTIONAL:
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
#
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
# Function to parse a FortiGate API response
_fortigate_parse_response() {
_fortigate_response="$1"
_fortigate_func="$2"
_fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4)
if [ "$_fortigate_status" != "success" ]; then
_err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
return 1
fi
_debug "[$_fortigate_func] Operation successful."
return 0
}
# Function to deploy a base64-encoded certificate to the firewall
_fortigate_deployer() {
_fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n')
_fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n')
_fortigate_payload=$(
cat <<EOF
{
"type": "regular",
"scope": "global",
"certname": "$_fortigate_cert_name",
"key_file_content": "$_fortigate_key_base64",
"file_content": "$_fortigate_cert_base64"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
_debug "Uploading certificate via URL: $_fortigate_url"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
_debug "FortiGate API Response: $_fortigate_response"
_fortigate_parse_response "$_fortigate_response" "Deploying certificate" || return 1
}
# Function to upload a CA certificate to the firewall
# FortiGate does not automatically extract the CA from the full chain.
_fortigate_upload_ca_cert() {
_fortigate_ca_base64=$(_base64 <"$_fortigate_cca" | tr -d '\n')
_fortigate_payload=$(
cat <<EOF
{
"import_method": "file",
"scope": "global",
"file_content": "$_fortigate_ca_base64"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
_debug "Uploading CA certificate via URL: $_fortigate_url"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
_debug "FortiGate API CA Response: $_fortigate_response"
# FortiGate error -328 means that the CA certificate already exists.
if echo "$_fortigate_response" | grep -q '"error":[ ]*-328'; then
_debug "CA certificate already exists. Skipping CA upload."
return 0
fi
_fortigate_parse_response "$_fortigate_response" "Deploying CA certificate" || return 1
}
# Function to activate the new certificate
_fortigate_set_active_web_cert() {
_fortigate_payload=$(
cat <<EOF
{
"admin-server-cert": "$_fortigate_cert_name"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
_debug "Setting GUI certificate..."
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "PUT" "application/json")
_fortigate_parse_response "$_fortigate_response" "Assigning active certificate" || return 1
}
# Function to clean up the previously deployed certificate
_fortigate_cleanup_previous_certificate() {
_getdeployconf FGT_LAST_CERT
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_fortigate_cert_name" ]; then
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "" "$_fortigate_url" "" "DELETE" "application/json")
_debug "Delete certificate API response: $_fortigate_response"
_fortigate_parse_response "$_fortigate_response" "Delete previous certificate" || return 1
else
_debug "No previous certificate found."
fi
}
# Main deploy-hook function
fortigate_deploy() {
# Include date and time to ensure unique names.
_fortigate_cert_name="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d_%H-%M-%S")"
_fortigate_ckey="$2"
_fortigate_cca="$4"
_fortigate_cfullchain="$5"
if [ ! -f "$_fortigate_ckey" ] || [ ! -f "$_fortigate_cfullchain" ]; then
_err "Valid key and/or certificate not found."
return 1
fi
# Save required environment variables if set; otherwise load saved values.
for _fortigate_var in FGT_HOST FGT_TOKEN FGT_PORT; do
if [ -n "$(eval echo "\$$_fortigate_var")" ]; then
_debug "Detected ENV variable $_fortigate_var. Saving to file."
_savedeployconf "$_fortigate_var" "$(eval echo "\$$_fortigate_var")" 1
else
_debug "Attempting to load variable $_fortigate_var from file."
_getdeployconf "$_fortigate_var"
fi
done
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
_err "FGT_HOST and FGT_TOKEN must be set."
return 1
fi
FGT_PORT="${FGT_PORT:-443}"
_debug "Using FortiGate port: $FGT_PORT"
# Upload the new certificate.
_fortigate_deployer || return 1
# Upload the CA certificate.
if [ -n "$_fortigate_cca" ] && [ -f "$_fortigate_cca" ]; then
_fortigate_upload_ca_cert || return 1
else
_debug "No CA certificate provided."
fi
# Activate the new certificate.
_fortigate_set_active_web_cert || return 1
# Delete the previously deployed certificate only after successful activation.
_fortigate_cleanup_previous_certificate || return 1
# Save the new certificate name for cleanup during the next deployment.
_savedeployconf "FGT_LAST_CERT" "$_fortigate_cert_name" 1
}
-126
View File
@@ -1,126 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to an AVM FRITZ!Box router.
#returns 0 means success, otherwise error.
#DEPLOY_FRITZBOX_USERNAME="username"
#DEPLOY_FRITZBOX_PASSWORD="password"
#DEPLOY_FRITZBOX_URL="https://fritz.box"
# Kudos to wikrie at Github for his FRITZ!Box update script:
# https://gist.github.com/wikrie/f1d5747a714e0a34d0582981f7cb4cfb
######## Public functions #####################
#domain keyfile certfile cafile fullchain
fritzbox_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists iconv; then
if ! _exists uconv; then
if ! _exists perl; then
_err "iconv or uconv or perl not found"
return 1
fi
fi
fi
# Clear traces of incorrectly stored values
_clearaccountconf DEPLOY_FRITZBOX_USERNAME
_clearaccountconf DEPLOY_FRITZBOX_PASSWORD
_clearaccountconf DEPLOY_FRITZBOX_URL
# Read config from saved values or env
_getdeployconf DEPLOY_FRITZBOX_USERNAME
_getdeployconf DEPLOY_FRITZBOX_PASSWORD
_getdeployconf DEPLOY_FRITZBOX_URL
_debug DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL"
_debug DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME"
_secure_debug DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD"
if [ -z "$DEPLOY_FRITZBOX_USERNAME" ]; then
_err "FRITZ!Box username is not found, please define DEPLOY_FRITZBOX_USERNAME."
return 1
fi
if [ -z "$DEPLOY_FRITZBOX_PASSWORD" ]; then
_err "FRITZ!Box password is not found, please define DEPLOY_FRITZBOX_PASSWORD."
return 1
fi
if [ -z "$DEPLOY_FRITZBOX_URL" ]; then
_err "FRITZ!Box url is not found, please define DEPLOY_FRITZBOX_URL."
return 1
fi
# Save current values
_savedeployconf DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME"
_savedeployconf DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD"
_savedeployconf DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL"
# Do not check for a valid SSL certificate, because initially the cert is not valid, so it could not install the LE generated certificate
export HTTPS_INSECURE=1
_info "Log in to the FRITZ!Box"
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
if _exists iconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)"
elif _exists uconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)"
else
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
fi
_fritzbox_sid="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua?sid=0000000000000000&username=${DEPLOY_FRITZBOX_USERNAME}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*<SID>//' -e 's/<\/SID>.*$//')"
if [ -z "${_fritzbox_sid}" ] || [ "${_fritzbox_sid}" = "0000000000000000" ]; then
_err "Logging in to the FRITZ!Box failed. Please check username, password and URL."
return 1
fi
_info "Generate form POST request"
_post_request="$(_mktemp)"
_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
# _CERTPASSWORD_ is unset because Let's Encrypt certificates don't have a password. But if they ever do, here's the place to use it!
_CERTPASSWORD_=
{
printf -- "--"
printf -- "%s\r\n" "${_post_boundary}"
printf "Content-Disposition: form-data; name=\"sid\"\r\n\r\n%s\r\n" "${_fritzbox_sid}"
printf -- "--"
printf -- "%s\r\n" "${_post_boundary}"
printf "Content-Disposition: form-data; name=\"BoxCertPassword\"\r\n\r\n%s\r\n" "${_CERTPASSWORD_}"
printf -- "--"
printf -- "%s\r\n" "${_post_boundary}"
printf "Content-Disposition: form-data; name=\"BoxCertImportFile\"; filename=\"BoxCert.pem\"\r\n"
printf "Content-Type: application/octet-stream\r\n\r\n"
cat "${_ckey}" "${_cfullchain}"
printf "\r\n"
printf -- "--"
printf -- "%s--" "${_post_boundary}"
} >>"${_post_request}"
_info "Upload certificate to the FRITZ!Box"
export _H1="Content-type: multipart/form-data boundary=${_post_boundary}"
_post "$(cat "${_post_request}")" "${DEPLOY_FRITZBOX_URL}/cgi-bin/firmwarecfg" | grep SSL
retval=$?
if [ $retval = 0 ]; then
_info "Upload successful"
else
_err "Upload failed"
fi
rm "${_post_request}"
return $retval
}
-143
View File
@@ -1,143 +0,0 @@
#!/usr/bin/env sh
# Here is the script to deploy the cert to G-Core CDN service (https://gcore.com/) using the G-Core Labs API (https://apidocs.gcore.com/cdn).
# Returns 0 when success.
#
# Written by temoffey <temofffey@gmail.com>
# Public domain, 2019
# Update by DreamOfIce <admin@dreamofice.cn> in 2023
#export DEPLOY_GCORE_CDN_USERNAME=myusername
#export DEPLOY_GCORE_CDN_PASSWORD=mypassword
######## Public functions #####################
#domain keyfile certfile cafile fullchain
gcore_cdn_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_fullchain=$(tr '\r\n' '*#' <"$_cfullchain" | sed 's/*#/#/g;s/##/#/g;s/#/\\n/g')
_key=$(tr '\r\n' '*#' <"$_ckey" | sed 's/*#/#/g;s/#/\\n/g')
_debug _fullchain "$_fullchain"
_debug _key "$_key"
if [ -z "$DEPLOY_GCORE_CDN_USERNAME" ]; then
if [ -z "$Le_Deploy_gcore_cdn_username" ]; then
_err "Please define the target username: export DEPLOY_GCORE_CDN_USERNAME=username"
return 1
fi
else
Le_Deploy_gcore_cdn_username="$DEPLOY_GCORE_CDN_USERNAME"
_savedomainconf Le_Deploy_gcore_cdn_username "$Le_Deploy_gcore_cdn_username"
fi
if [ -z "$DEPLOY_GCORE_CDN_PASSWORD" ]; then
if [ -z "$Le_Deploy_gcore_cdn_password" ]; then
_err "Please define the target password: export DEPLOY_GCORE_CDN_PASSWORD=password"
return 1
fi
else
Le_Deploy_gcore_cdn_password="$DEPLOY_GCORE_CDN_PASSWORD"
_savedomainconf Le_Deploy_gcore_cdn_password "$Le_Deploy_gcore_cdn_password"
fi
_info "Get authorization token"
_request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
_debug _request "$_request"
export _H1="Content-Type:application/json"
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
_debug _response "$_response"
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
_debug _regex "$_regex"
_token=$(echo "$_response" | sed -n "s/$_regex/\1/p")
_debug _token "$_token"
if [ -z "$_token" ]; then
_err "Error G-Core Labs API authorization"
return 1
fi
_info "Find CDN resource with cname $_cdomain"
export _H2="Authorization:Bearer $_token"
_response=$(_get "https://api.gcore.com/cdn/resources")
_debug _response "$_response"
_regex="\"primary_resource\":null},"
_debug _regex "$_regex"
_response=$(echo "$_response" | sed "s/$_regex/$_regex\n/g")
_debug _response "$_response"
_regex="^.*\"cname\":\"$_cdomain\".*$"
_debug _regex "$_regex"
_resource=$(echo "$_response" | _egrep_o "$_regex")
_debug _resource "$_resource"
_regex=".*\"id\":\([0-9]*\).*$"
_debug _regex "$_regex"
_resourceId=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
_debug _resourceId "$_resourceId"
_regex=".*\"sslData\":\([0-9]*\).*$"
_debug _regex "$_regex"
_sslDataOld=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
_debug _sslDataOld "$_sslDataOld"
_regex=".*\"originGroup\":\([0-9]*\).*$"
_debug _regex "$_regex"
_originGroup=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
_debug _originGroup "$_originGroup"
if [ -z "$_resourceId" ] || [ -z "$_originGroup" ]; then
_err "Not found CDN resource with cname $_cdomain"
return 1
fi
_info "Add new SSL certificate"
_date=$(date "+%d.%m.%Y %H:%M:%S")
_request="{\"name\":\"$_cdomain ($_date)\",\"sslCertificate\":\"$_fullchain\",\"sslPrivateKey\":\"$_key\"}"
_debug _request "$_request"
_response=$(_post "$_request" "https://api.gcore.com/cdn/sslData")
_debug _response "$_response"
_regex=".*\"id\":\([0-9]*\).*$"
_debug _regex "$_regex"
_sslDataAdd=$(echo "$_response" | sed -n "s/$_regex/\1/p")
_debug _sslDataAdd "$_sslDataAdd"
if [ -z "$_sslDataAdd" ]; then
_err "Error new SSL certificate add"
return 1
fi
_info "Update CDN resource"
_request="{\"originGroup\":$_originGroup,\"sslData\":$_sslDataAdd}"
_debug _request "$_request"
_response=$(_post "$_request" "https://api.gcore.com/cdn/resources/$_resourceId" '' "PUT")
_debug _response "$_response"
_regex=".*\"sslData\":\([0-9]*\).*$"
_debug _regex "$_regex"
_sslDataNew=$(echo "$_response" | sed -n "s/$_regex/\1/p")
_debug _sslDataNew "$_sslDataNew"
if [ "$_sslDataNew" != "$_sslDataAdd" ]; then
_err "Error CDN resource update"
return 1
fi
if [ -z "$_sslDataOld" ] || [ "$_sslDataOld" = "null" ]; then
_info "Not found old SSL certificate"
else
_info "Delete old SSL certificate"
_response=$(_post '' "https://api.gcore.com/cdn/sslData/$_sslDataOld" '' "DELETE")
_debug _response "$_response"
fi
_info "Certificate successfully deployed"
return 0
}
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificate to a Gitlab hosted page
# The following variables exported from environment will be used.
# If not set then values previously saved in domain.conf file are used.
# All the variables are required
# export GITLAB_TOKEN="xxxxxxx"
# export GITLAB_PROJECT_ID=012345
# export GITLAB_DOMAIN="mydomain.com"
gitlab_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$GITLAB_TOKEN" ]; then
if [ -z "$Le_Deploy_gitlab_token" ]; then
_err "GITLAB_TOKEN not defined."
return 1
fi
else
Le_Deploy_gitlab_token="$GITLAB_TOKEN"
_savedomainconf Le_Deploy_gitlab_token "$Le_Deploy_gitlab_token"
fi
if [ -z "$GITLAB_PROJECT_ID" ]; then
if [ -z "$Le_Deploy_gitlab_project_id" ]; then
_err "GITLAB_PROJECT_ID not defined."
return 1
fi
else
Le_Deploy_gitlab_project_id="$GITLAB_PROJECT_ID"
_savedomainconf Le_Deploy_gitlab_project_id "$Le_Deploy_gitlab_project_id"
fi
if [ -z "$GITLAB_DOMAIN" ]; then
if [ -z "$Le_Deploy_gitlab_domain" ]; then
_err "GITLAB_DOMAIN not defined."
return 1
fi
else
Le_Deploy_gitlab_domain="$GITLAB_DOMAIN"
_savedomainconf Le_Deploy_gitlab_domain "$Le_Deploy_gitlab_domain"
fi
string_fullchain=$(_url_encode <"$_cfullchain")
string_key=$(_url_encode <"$_ckey")
body="certificate=$string_fullchain&key=$string_key"
export _H1="PRIVATE-TOKEN: $Le_Deploy_gitlab_token"
gitlab_url="https://gitlab.com/api/v4/projects/$Le_Deploy_gitlab_project_id/pages/domains/$Le_Deploy_gitlab_domain"
_response=$(_post "$body" "$gitlab_url" 0 PUT | _dbase64 "multiline")
error_response="error"
if test "${_response#*"$error_response"}" != "$_response"; then
_err "Error in deploying certificate:"
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Certificate successfully deployed"
return 0
}
-411
View File
@@ -1,411 +0,0 @@
#!/usr/bin/env sh
# Script for acme.sh to deploy certificates to haproxy
#
# The following variables can be exported:
#
# export DEPLOY_HAPROXY_PEM_NAME="${domain}.pem"
#
# Defines the name of the PEM file.
# Defaults to "<domain>.pem"
#
# export DEPLOY_HAPROXY_PEM_PATH="/etc/haproxy"
#
# Defines location of PEM file for HAProxy.
# Defaults to /etc/haproxy
#
# export DEPLOY_HAPROXY_RELOAD="systemctl reload haproxy"
#
# OPTIONAL: Reload command used post deploy
# This defaults to be a no-op (ie "true").
# It is strongly recommended to set this something that makes sense
# for your distro.
#
# export DEPLOY_HAPROXY_ISSUER="no"
#
# OPTIONAL: Places CA file as "${DEPLOY_HAPROXY_PEM}.issuer"
# Note: Required for OCSP stapling to work
#
# export DEPLOY_HAPROXY_BUNDLE="no"
#
# OPTIONAL: Deploy this certificate as part of a multi-cert bundle
# This adds a suffix to the certificate based on the certificate type
# eg RSA certificates will have .rsa as a suffix to the file name
# HAProxy will load all certificates and provide one or the other
# depending on client capabilities
# Note: This functionality requires HAProxy was compiled against
# a version of OpenSSL that supports this.
#
# export DEPLOY_HAPROXY_HOT_UPDATE="yes"
# export DEPLOY_HAPROXY_STATS_SOCKET="UNIX:/run/haproxy/admin.sock"
#
# OPTIONAL: Deploy the certificate over the HAProxy stats socket without
# needing to reload HAProxy. Default is "no".
#
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
# address format. The certificate can be deployed to a comma separated ',' list
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
#
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
#
# OPTIONAL: To use the master CLI with DEPLOY_HAPROXY_HOT_UPDATE="yes" instead
# of a stats socket, use this variable.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
haproxy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cmdpfx=""
# Some defaults
DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy"
DEPLOY_HAPROXY_PEM_NAME_DEFAULT="${_cdomain}.pem"
DEPLOY_HAPROXY_BUNDLE_DEFAULT="no"
DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT="no"
DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT="UNIX:/run/haproxy/admin.sock"
_debug _cdomain "${_cdomain}"
_debug _ckey "${_ckey}"
_debug _ccert "${_ccert}"
_debug _cca "${_cca}"
_debug _cfullchain "${_cfullchain}"
# PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_PEM_PATH
_debug2 DEPLOY_HAPROXY_PEM_PATH "${DEPLOY_HAPROXY_PEM_PATH}"
if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then
Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
_savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}"
elif [ -z "${Le_Deploy_haproxy_pem_path}" ]; then
Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
fi
# Ensure PEM_PATH exists
if [ -d "${Le_Deploy_haproxy_pem_path}" ]; then
_debug "PEM_PATH ${Le_Deploy_haproxy_pem_path} exists"
else
_err "PEM_PATH ${Le_Deploy_haproxy_pem_path} does not exist"
return 1
fi
# PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_PEM_NAME
_debug2 DEPLOY_HAPROXY_PEM_NAME "${DEPLOY_HAPROXY_PEM_NAME}"
if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then
Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}"
_savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then
Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
# We better not have '*' as the first character
if [ "${Le_Deploy_haproxy_pem_name%%"${Le_Deploy_haproxy_pem_name#?}"}" = '*' ]; then
# removes the first characters and add a _ instead
Le_Deploy_haproxy_pem_name="_${Le_Deploy_haproxy_pem_name#?}"
fi
fi
# BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_BUNDLE
_debug2 DEPLOY_HAPROXY_BUNDLE "${DEPLOY_HAPROXY_BUNDLE}"
if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then
Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}"
_savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}"
elif [ -z "${Le_Deploy_haproxy_bundle}" ]; then
Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
fi
# ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_ISSUER
_debug2 DEPLOY_HAPROXY_ISSUER "${DEPLOY_HAPROXY_ISSUER}"
if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then
Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}"
_savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}"
elif [ -z "${Le_Deploy_haproxy_issuer}" ]; then
Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
fi
# RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_RELOAD
_debug2 DEPLOY_HAPROXY_RELOAD "${DEPLOY_HAPROXY_RELOAD}"
if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then
Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}"
_savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}"
elif [ -z "${Le_Deploy_haproxy_reload}" ]; then
Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
fi
# HOT_UPDATE is optional. If not provided then assume "${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_HOT_UPDATE
_debug2 DEPLOY_HAPROXY_HOT_UPDATE "${DEPLOY_HAPROXY_HOT_UPDATE}"
if [ -n "${DEPLOY_HAPROXY_HOT_UPDATE}" ]; then
Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE}"
_savedomainconf Le_Deploy_haproxy_hot_update "${Le_Deploy_haproxy_hot_update}"
elif [ -z "${Le_Deploy_haproxy_hot_update}" ]; then
Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}"
fi
# STATS_SOCKET is optional. If not provided then assume "${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}"
_getdeployconf DEPLOY_HAPROXY_STATS_SOCKET
_debug2 DEPLOY_HAPROXY_STATS_SOCKET "${DEPLOY_HAPROXY_STATS_SOCKET}"
if [ -n "${DEPLOY_HAPROXY_STATS_SOCKET}" ]; then
Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET}"
_savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}"
elif [ -z "${Le_Deploy_haproxy_stats_socket}" ]; then
Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}"
fi
# MASTER_CLI is optional. No defaults are used. When the master CLI is used,
# all commands are sent with a prefix.
_getdeployconf DEPLOY_HAPROXY_MASTER_CLI
_debug2 DEPLOY_HAPROXY_MASTER_CLI "${DEPLOY_HAPROXY_MASTER_CLI}"
if [ -n "${DEPLOY_HAPROXY_MASTER_CLI}" ]; then
Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_MASTER_CLI}"
_savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}"
_cmdpfx="@1 " # command prefix used for master CLI only.
fi
# Set the suffix depending if we are creating a bundle or not
if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
_info "Bundle creation requested"
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
if _isEccKey "${Le_Keylength}"; then
_info "ECC key type detected"
_suffix=".ecdsa"
else
_info "RSA key type detected"
_suffix=".rsa"
fi
else
_suffix=""
fi
_debug _suffix "${_suffix}"
# Set variables for later
_pem="${Le_Deploy_haproxy_pem_path}/${Le_Deploy_haproxy_pem_name}${_suffix}"
_issuer="${_pem}.issuer"
_ocsp="${_pem}.ocsp"
_reload="${Le_Deploy_haproxy_reload}"
_info "Deploying PEM file"
# Create a temporary PEM file
_temppem="$(_mktemp)"
_debug _temppem "${_temppem}"
cat "${_ccert}" "${_cca}" "${_ckey}" | grep . >"${_temppem}"
_ret="$?"
# Check that we could create the temporary file
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned during PEM file creation"
[ -f "${_temppem}" ] && rm -f "${_temppem}"
return ${_ret}
fi
# Move PEM file into place
_info "Moving new certificate into place"
_debug _pem "${_pem}"
cat "${_temppem}" >"${_pem}"
_ret=$?
# Clean up temp file
[ -f "${_temppem}" ] && rm -f "${_temppem}"
# Deal with any failure of moving PEM file into place
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned while moving new certificate into place"
return ${_ret}
fi
# Update .issuer file if requested
if [ "${Le_Deploy_haproxy_issuer}" = "yes" ]; then
_info "Updating .issuer file"
_debug _issuer "${_issuer}"
cat "${_cca}" >"${_issuer}"
_ret="$?"
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned while copying issuer/CA certificate into place"
return ${_ret}
fi
else
[ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists"
fi
# Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option
if [ -z "${Le_OCSP_Staple}" ]; then
Le_OCSP_Staple="0"
fi
if [ "${Le_OCSP_Staple}" = "1" ]; then
_info "Updating OCSP stapling info"
_debug _ocsp "${_ocsp}"
_info "Extracting OCSP URL"
_ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
_debug _ocsp_url "${_ocsp_url}"
# Only process OCSP if URL was present
if [ "${_ocsp_url}" != "" ]; then
# Extract the hostname from the OCSP URL
_info "Extracting OCSP URL"
_ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3)
_debug _ocsp_host "${_ocsp_host}"
# Only process the certificate if we have a .issuer file
if [ -r "${_issuer}" ]; then
# Check if issuer cert is also a root CA cert
_subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
_debug _subjectdn "${_subjectdn}"
_issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
_debug _issuerdn "${_issuerdn}"
_info "Requesting OCSP response"
# If the issuer is a CA cert then our command line has "-CAfile" added
if [ "${_subjectdn}" = "${_issuerdn}" ]; then
_cafile_argument="-CAfile \"${_issuer}\""
else
_cafile_argument=""
fi
_debug _cafile_argument "${_cafile_argument}"
# OpenSSL 1.1+ expects -header Host=value (one argument), while
# LibreSSL keeps the old two-argument form -header Host value at any
# version (3.x/4.x), so it must be detected by name, not by number.
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
_debug _openssl_name "${_openssl_name}"
_debug _openssl_version "${_openssl_version}"
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
if [ "${_openssl_name}" = "LibreSSL" ]; then
_header_sep=" "
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
fi
# Request the OCSP response from the issuer and store it
_openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
-issuer \"${_issuer}\" \
-cert \"${_pem}\" \
-url \"${_ocsp_url}\" \
-header Host${_header_sep}\"${_ocsp_host}\" \
-respout \"${_ocsp}\" \
-verify_other \"${_issuer}\" \
${_cafile_argument} \
| grep -q \"${_pem}: good\""
_debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}"
eval "${_openssl_ocsp_cmd}"
_ret=$?
else
# Non fatal: No issuer file was present so no OCSP stapling file created
_err "OCSP stapling in use but no .issuer file was present"
fi
else
# Non fatal: No OCSP url was found int the certificate
_err "OCSP update requested but no OCSP URL was found in certificate"
fi
# Non fatal: Check return code of openssl command
if [ "${_ret}" != "0" ]; then
_err "Updating OCSP stapling failed with return code ${_ret}"
fi
else
# An OCSP file was already present but certificate did not have OCSP extension
if [ -f "${_ocsp}" ]; then
_err "OCSP was not requested but .ocsp file exists."
# Could remove the file at this step, although HAProxy just ignores it in this case
# rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file"
fi
fi
if [ "${Le_Deploy_haproxy_hot_update}" = "yes" ]; then
# set the socket name for messages
if [ -n "${_cmdpfx}" ]; then
_socketname="master CLI"
else
_socketname="stats socket"
fi
# Update certificate over HAProxy stats socket or master CLI.
if _exists socat; then
IFS=','
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
# look for the certificate on the stats socket, to choose between updating or creating one
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
_debug _socat_cert_cmd "${_socat_cert_cmd}"
eval "${_socat_cert_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_newcert="1"
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
eval "${_socat_crtlist_show_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
fi
# create a new certificate
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
_debug _socat_new_cmd "${_socat_new_cmd}"
eval "${_socat_new_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't create '${_pem}' in haproxy"
return "${_ret}"
fi
else
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
eval "${_socat_cert_commit_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't commit '${_pem}' in haproxy"
return ${_ret}
fi
if [ "${_newcert}" = "1" ]; then
# if this is a new certificate, it needs to be inserted into the crt-list`
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
eval "${_socat_cert_add_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
done
else
_err "'socat' is not available, couldn't update over ${_socketname}"
fi
else
# Reload HAProxy
_debug _reload "${_reload}"
eval "${_reload}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} during reload"
return ${_ret}
else
_info "Reload successful"
fi
fi
return 0
}
-114
View File
@@ -1,114 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to ikuai using curl
#
# it requires following environment variables:
#
# IKUAI_SCHEME="http" - http or https , defaults to "http"
# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1"
# IKUAI_PORT="80" - port , defaults to "80"
# IKUAI_USERNAME="admin" - username , defaults to "admin"
# IKUAI_PASSWORD="yourPassword" - password
# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!!
#
#returns 0 means success, otherwise error.
#
######## Public functions #####################
#
#domain keyfile certfile cafile fullchain
ikuai_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Get deploy conf
_getdeployconf IKUAI_SCHEME
_getdeployconf IKUAI_HOSTNAME
_getdeployconf IKUAI_PORT
_getdeployconf IKUAI_USERNAME
_getdeployconf IKUAI_PASSWORD
_getdeployconf IKUAI_CERT_ID
# Use default if not provided
[ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http"
[ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1"
[ -n "$IKUAI_PORT" ] || IKUAI_PORT=80
[ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin"
[ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1
if [ -z "$IKUAI_PASSWORD" ]; then
_err "please define IKUAI_PASSWORD."
return 1
fi
_debug2 IKUAI_SCHEME "$IKUAI_SCHEME"
_debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_debug2 IKUAI_PORT "$IKUAI_PORT"
_debug2 IKUAI_USERNAME "$IKUAI_USERNAME"
_secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD"
_info "Login to ikuai ..."
_ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT"
_pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)"
_pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)"
_debug2 _ikuai_url "$_ikuai_url"
_login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}"
_response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to login to ikuai: $_err_msg"
return 1
fi
# check cookie
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')"
if [ -z "$_cookie" ]; then
_err "Fail to get the cookie."
return 1
fi
# Set cookie header
_H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1"
_info "Deploy the cert to ikuai ... "
# Should replace \n to @ ," " to #
_cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')"
_key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')"
_debug2 _cert_content_single_line "$_cert_content_single_line"
_secure_debug2 _key_content_single_line "$_key_content_single_line"
_key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}"
_response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to deploy the cert to ikuai: $_err_msg"
return 1
fi
_info "Save the deploy config ... "
# Save the config
_savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME"
_savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_savedeployconf IKUAI_PORT "$IKUAI_PORT"
_savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME"
_savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD"
_savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID"
_info "Successfully deployed certificate to ikuai. Enjoy! :>"
return 0
}
-253
View File
@@ -1,253 +0,0 @@
#!/usr/bin/env sh
# Script to deploy a certificate to a JetKVM (https://jetkvm.com) KVM-over-IP
# device over SSH. See also:
# https://github.com/acmesh-official/acme.sh/wiki/deployhooks
#
# JetKVM only supports key-based SSH authentication (root@<device>, password
# logins are disabled) once "Developer Mode" is enabled and a public key is
# pasted into its web UI (Settings > Advanced). SSH keys must already be
# exchanged and a passwordless login confirmed working (e.g. `ssh
# root@jetkvm.example.com true`) before using this hook.
#
# JetKVM's minimal userspace does not ship an scp binary or SFTP server, so
# unlike deploy/ssh.sh this hook has no "use scp" option: it always writes
# the certificate and key by piping a small POSIX shell script to the
# remote "sh" over stdin (only depends on "sh", "cat", "chmod", "mkdir",
# "mv" and "rm" on the device side). The remote path, filenames and file
# permissions are firmware constants on this single-purpose, single-root
# appliance, so they are not configurable here.
#
# JetKVM's "Custom" TLS mode (device web UI: Settings > Network > HTTPS
# Mode, must already be set to "Custom" before this hook's uploads take
# effect) reads the certificate/key from that fixed location and does not
# hot-reload: a device reboot is required to pick up a new certificate.
# This hook's restart command therefore defaults to "reboot" -- a blank
# DEPLOY_JETKVM_RESTART_CMD is treated the same as unset (falls back to
# "reboot") rather than silently skipping it, since a renewed certificate
# that's never actually applied defeats the point of automating this; set
# it to the literal value "none" to opt out and apply/verify manually.
# The restart command is run detached on the device (nohup ... &) so this
# hook's ssh call can return before the reboot itself lands, rather than
# racing the connection teardown.
#
# The certificate and key are staged under fixed temporary names on the
# device and only renamed into their final names (an atomic "mv", on the
# same filesystem) once both have been fully written and chmod'ed. This
# keeps a dropped connection or a failed write from ever leaving the
# device with a truncated or mismatched certificate/key pair for its own
# HTTPS listener, and a "trap ... EXIT" in the generated script removes
# any leftover staged file however that script exits.
#
# Before writing anything, this hook also checks that the device's HTTPS
# Mode is already "Custom" -- uploading a certificate that mode won't
# even serve would otherwise be a silent no-op. There is currently no
# documented/headless way to read this back (JetKVM's own JSON-RPC
# getTLSState/setTLSState calls require an authenticated WebRTC session,
# see https://github.com/jetkvm/kvm/issues/1240 and the still-open
# https://github.com/jetkvm/kvm/pull/1515), so this greps the device's
# own config file instead: JetKVM's firmware (see web_tls.go / config.go
# in https://github.com/jetkvm/kvm) persists the mode as the plain-JSON
# field "tls_mode" (values "", "self-signed", or "custom") in
# /userdata/kvm_config.json.
#
# None of the above (storage path, filenames, config file, reboot-to-apply
# behavior) is part of JetKVM's stable/documented API; it was confirmed
# against real JetKVM hardware, but is worth a spot-check after a JetKVM
# firmware upgrade -- set DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE=no to skip the
# HTTPS-mode check entirely if a future firmware version changes that
# file's format out from under it.
#
# The following variables exported from environment will be used. If not
# set then values previously saved in the domain.conf file are used. All
# of them are optional.
#
# export DEPLOY_JETKVM_USER="root" # defaults to "root"
# export DEPLOY_JETKVM_HOST="jetkvm.example.com" # defaults to the cert's domain
# export DEPLOY_JETKVM_PORT="22" # defaults to 22
# export DEPLOY_JETKVM_SSH_CMD="ssh -T" # defaults to "ssh -T"
# export DEPLOY_JETKVM_RESTART_CMD="reboot" # defaults to "reboot"; set to "none" to skip it
# export DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes" # defaults to "yes" (verify tls_mode=custom before upload); set to "no" to skip
#
# Example:
# ```sh
# export DEPLOY_JETKVM_HOST="192.168.1.50"
# acme.sh --deploy -d jetkvm.example.com --deploy-hook jetkvm
# ```
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
jetkvm_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ ! -s "$_ckey" ] || [ ! -s "$_cfullchain" ]; then
_err "JetKVM deploy needs both a private key and a fullchain certificate (not available, e.g., after --signcsr)."
return 1
fi
_getdeployconf DEPLOY_JETKVM_USER
if [ -z "$DEPLOY_JETKVM_USER" ]; then
DEPLOY_JETKVM_USER="root"
fi
_savedeployconf DEPLOY_JETKVM_USER "$DEPLOY_JETKVM_USER"
_getdeployconf DEPLOY_JETKVM_HOST
if [ -z "$DEPLOY_JETKVM_HOST" ]; then
_debug "Using _cdomain as DEPLOY_JETKVM_HOST, please set if not correct."
DEPLOY_JETKVM_HOST="$_cdomain"
fi
_savedeployconf DEPLOY_JETKVM_HOST "$DEPLOY_JETKVM_HOST"
_getdeployconf DEPLOY_JETKVM_PORT
if [ -z "$DEPLOY_JETKVM_PORT" ]; then
DEPLOY_JETKVM_PORT="22"
fi
_savedeployconf DEPLOY_JETKVM_PORT "$DEPLOY_JETKVM_PORT"
_getdeployconf DEPLOY_JETKVM_SSH_CMD
if [ -z "$DEPLOY_JETKVM_SSH_CMD" ]; then
DEPLOY_JETKVM_SSH_CMD="ssh -T"
fi
_savedeployconf DEPLOY_JETKVM_SSH_CMD "$DEPLOY_JETKVM_SSH_CMD" "base64"
_getdeployconf DEPLOY_JETKVM_RESTART_CMD
if [ -z "$DEPLOY_JETKVM_RESTART_CMD" ]; then
DEPLOY_JETKVM_RESTART_CMD="reboot"
fi
_savedeployconf DEPLOY_JETKVM_RESTART_CMD "$DEPLOY_JETKVM_RESTART_CMD" "base64"
_getdeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE
if [ -z "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" ]; then
DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes"
fi
_savedeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE"
_info "Deploying certificate to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT"
# Firmware constants on a single-purpose, single-root appliance -- not
# user configuration. If JetKVM ever moves these, that's a hook update,
# not a setting (a saved-per-domain override would just as easily hide
# the fix from anyone already using this hook).
_jetkvm_remote_path="/userdata/jetkvm/tls"
_jetkvm_cert_name="user-defined.crt"
_jetkvm_key_name="user-defined.key"
_jetkvm_config_file="/userdata/kvm_config.json"
_jetkvm_mode_exitcode=3
_jetkvm_config_missing_exitcode=4
_jetkvm_run_id="$$.$(_time)"
_jetkvm_cert_marker="ACME_JETKVM_CERT_$_jetkvm_run_id"
_jetkvm_key_marker="ACME_JETKVM_KEY_$_jetkvm_run_id"
_jetkvm_cert_tmp="$_jetkvm_remote_path/.$_jetkvm_cert_name.tmp"
_jetkvm_key_tmp="$_jetkvm_remote_path/.$_jetkvm_key_name.tmp"
_jetkvm_cert_target="$_jetkvm_remote_path/$_jetkvm_cert_name"
_jetkvm_key_target="$_jetkvm_remote_path/$_jetkvm_key_name"
# Command substitution strips all trailing newlines, so the printf below
# always emits the content with exactly one trailing newline before the
# heredoc terminator -- regardless of whether the source file already
# ended with one -- so the terminator is guaranteed to start its own line.
_jetkvm_cert_content="$(cat "$_cfullchain")"
_jetkvm_key_content="$(cat "$_ckey")"
_jetkvm_upload_script="$(
echo "#!/bin/sh"
echo "set -e"
echo "umask 077"
printf "trap \"rm -f '%s' '%s'\" EXIT\n" "$_jetkvm_cert_tmp" "$_jetkvm_key_tmp"
if [ "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" != "no" ]; then
# Uploading a certificate that HTTPS Mode won't even serve would
# otherwise fail silently -- see the header comment for why this
# greps the device's own config file rather than querying it
# through a documented API (there isn't one for reading this
# headlessly yet). The config file is checked for readability
# separately so a missing/renamed file isn't misreported as
# HTTPS Mode being wrong.
printf "if [ ! -r '%s' ]; then exit %s; fi\n" "$_jetkvm_config_file" "$_jetkvm_config_missing_exitcode"
printf 'if ! grep -q '\''"tls_mode" *: *"custom"'\'' '\''%s'\''; then exit %s; fi\n' "$_jetkvm_config_file" "$_jetkvm_mode_exitcode"
fi
printf "mkdir -p '%s'\n" "$_jetkvm_remote_path"
printf "cat > '%s' <<'%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_marker"
printf '%s\n' "$_jetkvm_cert_content"
echo "$_jetkvm_cert_marker"
printf "chmod 0644 '%s'\n" "$_jetkvm_cert_tmp"
printf "cat > '%s' <<'%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_marker"
printf '%s\n' "$_jetkvm_key_content"
echo "$_jetkvm_key_marker"
printf "chmod 0600 '%s'\n" "$_jetkvm_key_tmp"
printf "mv '%s' '%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_target"
printf "mv '%s' '%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_target"
)"
_secure_debug "Generated upload script" "$_jetkvm_upload_script"
_info "Connecting to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT to deploy certificate"
# shellcheck disable=SC2086
printf '%s\n' "$_jetkvm_upload_script" | $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" sh
_ret=$?
if [ "$_ret" = "$_jetkvm_config_missing_exitcode" ]; then
_err "JetKVM config file ($_jetkvm_config_file) was not found or not readable on the device -- this hook's assumptions may be out of date after a firmware upgrade. Certificate was NOT uploaded."
return "$_ret"
fi
if [ "$_ret" = "$_jetkvm_mode_exitcode" ]; then
_err "JetKVM HTTPS Mode is not set to \"Custom\" (checked \"tls_mode\" in $_jetkvm_config_file on the device). Set it in the device's web UI (Settings > Network > HTTPS Mode) before this hook can take effect. Certificate was NOT uploaded."
return "$_ret"
fi
if [ "$_ret" != "0" ]; then
_err "Error code $_ret returned uploading certificate to JetKVM device"
return "$_ret"
fi
_info "Certificate and key uploaded to $_jetkvm_remote_path on the device"
if [ "$DEPLOY_JETKVM_RESTART_CMD" = "none" ]; then
_info "Certificate successfully deployed to JetKVM device. DEPLOY_JETKVM_RESTART_CMD=none, skipping restart command."
return 0
fi
# Run the restart command detached (nohup ... &) so this ssh call
# returns as soon as it's launched, before the device actually reboots,
# rather than racing the connection teardown -- observed, against real
# hardware, that a reboot racing the SSH session's own exit can make
# ssh itself exit anywhere from a clean 0 to a connection-reset 255.
# Since the restart command then runs as an unwaited background job on
# the device, this ssh call reports success as soon as that job is
# launched -- it does NOT confirm nohup, sh, or the restart command
# itself actually exist or succeed (measured: a nonexistent restart
# command, and even a missing nohup binary, both still return 0 here).
# Only an outright SSH connection failure (unreachable host, auth
# failure, etc.) is caught below. "sleep" runs on the device's own
# shell, not acme.sh's, so acme.sh's _sleep wrapper does not apply.
_info "Running post-upload command on JetKVM device: $DEPLOY_JETKVM_RESTART_CMD"
# Escape any single quotes in the (user-configurable, free-text)
# restart command before nesting it inside the outer 'sleep N; ...'
# single-quoted string -- otherwise a value like "sh -c 'sync; reboot'"
# breaks the quoting and only part of it ends up inside the detached
# background job.
_jetkvm_restart_cmd_escaped=$(printf '%s' "$DEPLOY_JETKVM_RESTART_CMD" | sed "s/'/'\\\\''/g")
_jetkvm_detached_cmd="nohup sh -c 'sleep 2; $_jetkvm_restart_cmd_escaped' >/dev/null 2>&1 &"
# shellcheck disable=SC2086
if ! $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" "$_jetkvm_detached_cmd"; then
_err "Certificate was uploaded, but connecting to the JetKVM device to launch the restart command failed."
return 1
fi
_info "Certificate deployed to JetKVM device; it will restart shortly to apply it."
return 0
}
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to a Kemp Loadmaster.
#returns 0 means success, otherwise error.
#DEPLOY_KEMP_TOKEN="token"
#DEPLOY_KEMP_URL="https://kemplm.example.com"
######## Public functions #####################
#domain keyfile certfile cafile fullchain
kemplm_deploy() {
_domain="$1"
_key_file="$2"
_cert_file="$3"
_ca_file="$4"
_fullchain_file="$5"
_debug _domain "$_domain"
_debug _key_file "$_key_file"
_debug _cert_file "$_cert_file"
_debug _ca_file "$_ca_file"
_debug _fullchain_file "$_fullchain_file"
if ! _exists jq; then
_err "jq not found"
return 1
fi
# Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename
_kemp_domain=$(echo "${_domain}" | sed 's/\*\.//')
_debug _kemp_domain "$_kemp_domain"
# Read config from saved values or env
_getdeployconf DEPLOY_KEMP_TOKEN
_getdeployconf DEPLOY_KEMP_URL
_debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
_secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
if [ -z "$DEPLOY_KEMP_TOKEN" ]; then
_err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN."
return 1
fi
if [ -z "$DEPLOY_KEMP_URL" ]; then
_err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL."
return 1
fi
# Save current values
_savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
_savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
# Check if certificate is already installed
_info "Check if certificate is already present"
_list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}"
_debug3 _list_request "${_list_request}"
_kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "^${_kemp_domain}$")
_debug2 _kemp_cert_count "${_kemp_cert_count}"
_kemp_replace_cert=1
if [ "${_kemp_cert_count}" -eq 0 ]; then
_kemp_replace_cert=0
_info "Certificate does not exist on Kemp Loadmaster"
else
_info "Certificate already exists on Kemp Loadmaster"
fi
_debug _kemp_replace_cert "${_kemp_replace_cert}"
# Upload new certificate to Kemp Loadmaster
_kemp_upload_cert=$(_mktemp)
cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}"
_info "Uploading certificate to Kemp Loadmaster"
_add_data=$(cat "${_kemp_upload_cert}")
_add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}"
_debug3 _add_request "${_add_request}"
_kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2")
_retval=$?
_debug2 _kemp_post_result "${_kemp_post_result}"
if [ "${_retval}" -eq 0 ]; then
_kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status')
_kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message')
if [ "${_kemp_post_status}" = "ok" ]; then
_info "Upload successful"
else
_err "Upload failed: ${_kemp_post_message}"
_retval=1
fi
else
_err "Upload failed"
_retval=1
fi
rm "${_kemp_upload_cert}"
return $_retval
}
-25
View File
@@ -1,25 +0,0 @@
#!/usr/bin/env sh
######## Public functions #####################
#domain keyfile certfile cafile fullchain
keychain_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
/usr/bin/security import "$_ckey" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_ccert" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_cca" -k "/Library/Keychains/System.keychain"
/usr/bin/security import "$_cfullchain" -k "/Library/Keychains/System.keychain"
return 0
}
-131
View File
@@ -1,131 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificate to KeyHelp
# This deployment required following variables
# export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com"
# export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username"
# export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password"
# export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID"
# Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID.
# https://DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=xxx
# If have more than one domain name
# export DEPLOY_KEYHELP_DOMAIN_ID="111 222 333"
keyhelp_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$DEPLOY_KEYHELP_BASEURL" ]; then
_err "DEPLOY_KEYHELP_BASEURL is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_BASEURL "$DEPLOY_KEYHELP_BASEURL"
fi
if [ -z "$DEPLOY_KEYHELP_USERNAME" ]; then
_err "DEPLOY_KEYHELP_USERNAME is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_USERNAME "$DEPLOY_KEYHELP_USERNAME"
fi
if [ -z "$DEPLOY_KEYHELP_PASSWORD" ]; then
_err "DEPLOY_KEYHELP_PASSWORD is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_PASSWORD "$DEPLOY_KEYHELP_PASSWORD"
fi
if [ -z "$DEPLOY_KEYHELP_DOMAIN_ID" ]; then
_err "DEPLOY_KEYHELP_DOMAIN_ID is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID"
fi
# Optional DEPLOY_KEYHELP_ENFORCE_HTTPS
_getdeployconf DEPLOY_KEYHELP_ENFORCE_HTTPS
# set default values for DEPLOY_KEYHELP_ENFORCE_HTTPS
[ -n "${DEPLOY_KEYHELP_ENFORCE_HTTPS}" ] || DEPLOY_KEYHELP_ENFORCE_HTTPS="1"
_info "Logging in to keyhelp panel"
username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)"
password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)"
_H1="Content-Type: application/x-www-form-urlencoded"
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?submit=1&username=$username_encoded&password=$password_encoded" "TRUE")
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)"
# If cookies is not empty then logon successful
if [ -z "$_cookie" ]; then
_err "Fail to get cookie."
return 1
fi
_debug "cookie" "$_cookie"
_info "Uploading certificate"
_date=$(date +"%Y%m%d")
encoded_key="$(_url_encode <"$_ckey")"
encoded_ccert="$(_url_encode <"$_ccert")"
encoded_cca="$(_url_encode <"$_cca")"
certificate_name="$_cdomain-$_date"
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
return 1
fi
for DOMAIN_ID in $DEPLOY_KEYHELP_DOMAIN_ID; do
_info "Apply certificate to domain id $DOMAIN_ID"
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=$DOMAIN_ID")
cert_value=$(echo "$_response" | grep "$certificate_name" | sed -n 's/.*value="\([^"]*\).*/\1/p')
target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p')
if [ "$target_type" = "directory" ]; then
path=$(echo "$_response" | awk '/name="path"/{getline; print}' | sed -n 's/.*value="\([^"]*\).*/\1/p')
fi
echo "$_response" | grep "is_prefer_https" | grep "checked" >/dev/null
if [ $? -eq 0 ]; then
is_prefer_https=1
else
is_prefer_https=0
fi
echo "$_response" | grep "hsts_enabled" | grep "checked" >/dev/null
if [ $? -eq 0 ]; then
hsts_enabled=1
else
hsts_enabled=0
fi
_debug "cert_value" "$cert_value"
if [ -z "$cert_value" ]; then
_err "Fail to get certificate id."
return 1
fi
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."
return 1
fi
done
_info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID."
return 0
}
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env sh
keyhelp_api_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
# Read config from saved values or env
_getdeployconf DEPLOY_KEYHELP_HOST
_getdeployconf DEPLOY_KEYHELP_API_KEY
_debug DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
_secure_debug DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
if [ -z "$DEPLOY_KEYHELP_HOST" ]; then
_err "KeyHelp host not found, please define DEPLOY_KEYHELP_HOST."
return 1
fi
if [ -z "$DEPLOY_KEYHELP_API_KEY" ]; then
_err "KeyHelp api key not found, please define DEPLOY_KEYHELP_API_KEY."
return 1
fi
# Save current values
_savedeployconf DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
_savedeployconf DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
_request_key="$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')"
_request_cert="$(tr '\n' ':' <"$_ccert" | sed 's/:/\\n/g')"
_request_ca="$(tr '\n' ':' <"$_cca" | sed 's/:/\\n/g')"
_request_body="{
\"name\": \"$_cdomain\",
\"components\": {
\"private_key\": \"$_request_key\",
\"certificate\": \"$_request_cert\",
\"ca_certificate\": \"$_request_ca\"
}
}"
_hosts="$(echo "$DEPLOY_KEYHELP_HOST" | tr "," " ")"
_keys="$(echo "$DEPLOY_KEYHELP_API_KEY" | tr "," " ")"
_i=1
for _host in $_hosts; do
_key="$(_getfield "$_keys" "$_i" " ")"
_i="$(_math "$_i" + 1)"
export _H1="X-API-Key: $_key"
_put_url="$_host/api/v2/certificates/name/$_cdomain"
if _post "$_request_body" "$_put_url" "" "PUT" "application/json" >/dev/null; then
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
else
_err "Cannot make PUT request to $_put_url"
return 1
fi
if [ "$_code" = "404" ]; then
_info "$_cdomain not found, creating new entry at $_host"
_post_url="$_host/api/v2/certificates"
if _post "$_request_body" "$_post_url" "" "POST" "application/json" >/dev/null; then
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
else
_err "Cannot make POST request to $_post_url"
return 1
fi
fi
if _startswith "$_code" "2"; then
_info "$_cdomain set at $_host"
else
_err "HTTP status code is $_code"
return 1
fi
done
return 0
}
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env sh
# If certificate already exists it will update only cert and key, not touching other parameters
# If certificate doesn't exist it will only upload cert and key, and not set other parameters
# Note that we deploy full chain
# Written by Geoffroi Genot <ggenot@voxbone.com>
######## Public functions #####################
#domain keyfile certfile cafile fullchain
kong_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_info "Deploying certificate on Kong instance"
if [ -z "$KONG_URL" ]; then
_debug "KONG_URL Not set, using default http://localhost:8001"
KONG_URL="http://localhost:8001"
fi
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
#Get ssl_uuid linked to the domain
ssl_uuid=$(_get "$KONG_URL/certificates/$_cdomain" | _normalizeJson | _egrep_o '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
if [ -z "$ssl_uuid" ]; then
_debug "Unable to get Kong ssl_uuid for domain $_cdomain"
_debug "Make sure that KONG_URL is correctly configured"
_debug "Make sure that a Kong certificate match the sni"
_debug "Kong url: $KONG_URL"
_info "No existing certificate, creating..."
#return 1
fi
#Save kong url if it's succesful (First run case)
_saveaccountconf KONG_URL "$KONG_URL"
#Generate DEIM
delim="-----MultipartDelimiter$(date "+%s%N")"
nl="\015\012"
#Set Header
_H1="Content-Type: multipart/form-data; boundary=$delim"
#Generate data for request (Multipart/form-data with mixed content)
if [ -z "$ssl_uuid" ]; then
#set sni to domain
content="--$delim${nl}Content-Disposition: form-data; name=\"snis[]\"${nl}${nl}$_cdomain"
fi
#add key
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
#Add cert
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
#Close multipart
content="$content${nl}--$delim--${nl}"
#Convert CRLF
content=$(printf %b "$content")
#DEBUG
_debug header "$_H1"
_debug content "$content"
#Check if sslcreated (if not => POST else => PATCH)
if [ -z "$ssl_uuid" ]; then
#Post certificate to Kong
response=$(_post "$content" "$KONG_URL/certificates" "" "POST")
else
#patch
response=$(_post "$content" "$KONG_URL/certificates/$ssl_uuid" "" "PATCH")
fi
if ! [ "$(echo "$response" | _egrep_o "created_at")" = "created_at" ]; then
_err "An error occurred with cert upload. Check response:"
_err "$response"
return 1
fi
_debug response "$response"
_info "Certificate successfully deployed"
}
-277
View File
@@ -1,277 +0,0 @@
#!/usr/bin/env sh
# Script for acme.sh to deploy certificates to lighttpd
#
# The following variables can be exported:
#
# export DEPLOY_LIGHTTPD_PEM_NAME="${domain}.pem"
#
# Defines the name of the PEM file.
# Defaults to "<domain>.pem"
#
# export DEPLOY_LIGHTTPD_PEM_PATH="/etc/lighttpd"
#
# Defines location of PEM file for Lighttpd.
# Defaults to /etc/lighttpd
#
# export DEPLOY_LIGHTTPD_RELOAD="systemctl reload lighttpd"
#
# OPTIONAL: Reload command used post deploy
# This defaults to be a no-op (ie "true").
# It is strongly recommended to set this something that makes sense
# for your distro.
#
# export DEPLOY_LIGHTTPD_ISSUER="yes"
#
# OPTIONAL: Places CA file as "${DEPLOY_LIGHTTPD_PEM}.issuer"
# Note: Required for OCSP stapling to work
#
# export DEPLOY_LIGHTTPD_BUNDLE="no"
#
# OPTIONAL: Deploy this certificate as part of a multi-cert bundle
# This adds a suffix to the certificate based on the certificate type
# eg RSA certificates will have .rsa as a suffix to the file name
# Lighttpd will load all certificates and provide one or the other
# depending on client capabilities
# Note: This functionality requires Lighttpd was compiled against
# a version of OpenSSL that supports this.
#
######## Public functions #####################
#domain keyfile certfile cafile fullchain
lighttpd_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
# Some defaults
DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT="/etc/lighttpd"
DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT="${_cdomain}.pem"
DEPLOY_LIGHTTPD_BUNDLE_DEFAULT="no"
DEPLOY_LIGHTTPD_ISSUER_DEFAULT="yes"
DEPLOY_LIGHTTPD_RELOAD_DEFAULT="true"
_debug _cdomain "${_cdomain}"
_debug _ckey "${_ckey}"
_debug _ccert "${_ccert}"
_debug _cca "${_cca}"
_debug _cfullchain "${_cfullchain}"
# PEM_PATH is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}"
_getdeployconf DEPLOY_LIGHTTPD_PEM_PATH
_debug2 DEPLOY_LIGHTTPD_PEM_PATH "${DEPLOY_LIGHTTPD_PEM_PATH}"
if [ -n "${DEPLOY_LIGHTTPD_PEM_PATH}" ]; then
Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH}"
_savedomainconf Le_Deploy_lighttpd_pem_path "${Le_Deploy_lighttpd_pem_path}"
elif [ -z "${Le_Deploy_lighttpd_pem_path}" ]; then
Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}"
fi
# Ensure PEM_PATH exists
if [ -d "${Le_Deploy_lighttpd_pem_path}" ]; then
_debug "PEM_PATH ${Le_Deploy_lighttpd_pem_path} exists"
else
_err "PEM_PATH ${Le_Deploy_lighttpd_pem_path} does not exist"
return 1
fi
# PEM_NAME is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}"
_getdeployconf DEPLOY_LIGHTTPD_PEM_NAME
_debug2 DEPLOY_LIGHTTPD_PEM_NAME "${DEPLOY_LIGHTTPD_PEM_NAME}"
if [ -n "${DEPLOY_LIGHTTPD_PEM_NAME}" ]; then
Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME}"
_savedomainconf Le_Deploy_lighttpd_pem_name "${Le_Deploy_lighttpd_pem_name}"
elif [ -z "${Le_Deploy_lighttpd_pem_name}" ]; then
Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}"
fi
# BUNDLE is optional. If not provided then assume "${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}"
_getdeployconf DEPLOY_LIGHTTPD_BUNDLE
_debug2 DEPLOY_LIGHTTPD_BUNDLE "${DEPLOY_LIGHTTPD_BUNDLE}"
if [ -n "${DEPLOY_LIGHTTPD_BUNDLE}" ]; then
Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE}"
_savedomainconf Le_Deploy_lighttpd_bundle "${Le_Deploy_lighttpd_bundle}"
elif [ -z "${Le_Deploy_lighttpd_bundle}" ]; then
Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}"
fi
# ISSUER is optional. If not provided then assume "${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}"
_getdeployconf DEPLOY_LIGHTTPD_ISSUER
_debug2 DEPLOY_LIGHTTPD_ISSUER "${DEPLOY_LIGHTTPD_ISSUER}"
if [ -n "${DEPLOY_LIGHTTPD_ISSUER}" ]; then
Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER}"
_savedomainconf Le_Deploy_lighttpd_issuer "${Le_Deploy_lighttpd_issuer}"
elif [ -z "${Le_Deploy_lighttpd_issuer}" ]; then
Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}"
fi
# RELOAD is optional. If not provided then assume "${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}"
_getdeployconf DEPLOY_LIGHTTPD_RELOAD
_debug2 DEPLOY_LIGHTTPD_RELOAD "${DEPLOY_LIGHTTPD_RELOAD}"
if [ -n "${DEPLOY_LIGHTTPD_RELOAD}" ]; then
Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD}"
_savedomainconf Le_Deploy_lighttpd_reload "${Le_Deploy_lighttpd_reload}"
elif [ -z "${Le_Deploy_lighttpd_reload}" ]; then
Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}"
fi
# Set the suffix depending if we are creating a bundle or not
if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then
_info "Bundle creation requested"
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
if _isEccKey "${Le_Keylength}"; then
_info "ECC key type detected"
_suffix=".ecdsa"
else
_info "RSA key type detected"
_suffix=".rsa"
fi
else
_suffix=""
fi
_debug _suffix "${_suffix}"
# Set variables for later
_pem="${Le_Deploy_lighttpd_pem_path}/${Le_Deploy_lighttpd_pem_name}${_suffix}"
_issuer="${_pem}.issuer"
_ocsp="${_pem}.ocsp"
_reload="${Le_Deploy_lighttpd_reload}"
_info "Deploying PEM file"
# Create a temporary PEM file
_temppem="$(_mktemp)"
_debug _temppem "${_temppem}"
cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}"
_ret="$?"
# Check that we could create the temporary file
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned during PEM file creation"
[ -f "${_temppem}" ] && rm -f "${_temppem}"
return ${_ret}
fi
# Move PEM file into place
_info "Moving new certificate into place"
_debug _pem "${_pem}"
cat "${_temppem}" >"${_pem}"
_ret=$?
# Clean up temp file
[ -f "${_temppem}" ] && rm -f "${_temppem}"
# Deal with any failure of moving PEM file into place
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned while moving new certificate into place"
return ${_ret}
fi
# Update .issuer file if requested
if [ "${Le_Deploy_lighttpd_issuer}" = "yes" ]; then
_info "Updating .issuer file"
_debug _issuer "${_issuer}"
cat "${_cca}" >"${_issuer}"
_ret="$?"
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} returned while copying issuer/CA certificate into place"
return ${_ret}
fi
else
[ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists"
fi
# Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option
if [ -z "${Le_OCSP_Staple}" ]; then
Le_OCSP_Staple="0"
fi
if [ "${Le_OCSP_Staple}" = "1" ]; then
_info "Updating OCSP stapling info"
_debug _ocsp "${_ocsp}"
_info "Extracting OCSP URL"
_ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
_debug _ocsp_url "${_ocsp_url}"
# Only process OCSP if URL was present
if [ "${_ocsp_url}" != "" ]; then
# Extract the hostname from the OCSP URL
_info "Extracting OCSP URL"
_ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3)
_debug _ocsp_host "${_ocsp_host}"
# Only process the certificate if we have a .issuer file
if [ -r "${_issuer}" ]; then
# Check if issuer cert is also a root CA cert
_subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
_debug _subjectdn "${_subjectdn}"
_issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
_debug _issuerdn "${_issuerdn}"
_info "Requesting OCSP response"
# If the issuer is a CA cert then our command line has "-CAfile" added
if [ "${_subjectdn}" = "${_issuerdn}" ]; then
_cafile_argument="-CAfile \"${_issuer}\""
else
_cafile_argument=""
fi
_debug _cafile_argument "${_cafile_argument}"
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
_debug _openssl_version "${_openssl_version}"
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
fi
# Request the OCSP response from the issuer and store it
_openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
-issuer \"${_issuer}\" \
-cert \"${_pem}\" \
-url \"${_ocsp_url}\" \
-header Host${_header_sep}\"${_ocsp_host}\" \
-respout \"${_ocsp}\" \
-verify_other \"${_issuer}\" \
${_cafile_argument} \
| grep -q \"${_pem}: good\""
_debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}"
eval "${_openssl_ocsp_cmd}"
_ret=$?
else
# Non fatal: No issuer file was present so no OCSP stapling file created
_err "OCSP stapling in use but no .issuer file was present"
fi
else
# Non fatal: No OCSP url was found int the certificate
_err "OCSP update requested but no OCSP URL was found in certificate"
fi
# Non fatal: Check return code of openssl command
if [ "${_ret}" != "0" ]; then
_err "Updating OCSP stapling failed with return code ${_ret}"
fi
else
# An OCSP file was already present but certificate did not have OCSP extension
if [ -f "${_ocsp}" ]; then
_err "OCSP was not requested but .ocsp file exists."
# Could remove the file at this step, although Lighttpd just ignores it in this case
# rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file"
fi
fi
# Reload Lighttpd
_debug _reload "${_reload}"
eval "${_reload}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Error code ${_ret} during reload"
return ${_ret}
else
_info "Reload successful"
fi
return 0
}
-157
View File
@@ -1,157 +0,0 @@
#!/usr/bin/env sh
# Deploy-hook to very simply copy files to set directories and then
# execute whatever reloadcmd the admin needs afterwards. This can be
# useful for configurations where the "multideploy" hook (in development)
# is used or when an admin wants ACME.SH to renew certs but needs to
# manually configure deployment via an external script
# (e.g. The deploy-freenas script for TrueNAS Core/Scale
# https://github.com/danb35/deploy-freenas/ )
#
# If the same file is configured for the certificate key
# and the certificate and/or full chain, a combined PEM file will
# be output instead.
#
# Environment variables to be utilized are as follows:
#
# DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key
# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer
# DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer
# DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer
# DEPLOY_LOCALCOPY_PFX - /path/to/target/cert.pfx
# DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'"
######## Public functions #####################
#domain keyfile certfile cafile fullchain
localcopy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE
_getdeployconf DEPLOY_LOCALCOPY_CERTKEY
_getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN
_getdeployconf DEPLOY_LOCALCOPY_CA
_getdeployconf DEPLOY_LOCALCOPY_RELOADCMD
_getdeployconf DEPLOY_LOCALCOPY_PFX
_combined_target=""
_combined_srccert=""
# Create PEM file
if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] &&
{ [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] ||
[ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then
_combined_target="$DEPLOY_LOCALCOPY_CERTKEY"
_savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY"
if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then
_combined_srccert="$_ccert"
_savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE"
DEPLOY_LOCALCOPY_CERTIFICATE=""
fi
if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then
_combined_srccert="$_cfullchain"
_savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN"
DEPLOY_LOCALCOPY_FULLCHAIN=""
fi
DEPLOY_LOCALCOPY_CERTKEY=""
_info "Creating combined PEM"
_debug "Creating combined PEM at $_combined_target"
if ! [ -f "$_combined_target" ]; then
touch "$_combined_target" || return 1
chmod 600 "$_combined_target"
fi
if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then
_err "Failed to create PEM file"
return 1
fi
fi
if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then
_info "Copying certificate"
_debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE"
if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then
_err "Failed to copy certificate, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE"
fi
if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then
_info "Copying certificate key"
_debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY"
if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then
touch "$DEPLOY_LOCALCOPY_CERTKEY" || return 1
chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY"
fi
if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then
_err "Failed to copy certificate key, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY"
fi
if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then
_info "Copying fullchain"
_debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN"
if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then
_err "Failed to copy fullchain, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN"
fi
if [ "$DEPLOY_LOCALCOPY_CA" ]; then
_info "Copying CA"
_debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA"
if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then
_err "Failed to copy CA, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA"
fi
if [ "$DEPLOY_LOCALCOPY_PFX" ]; then
_info "Copying PFX"
_debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX"
if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then
touch "$DEPLOY_LOCALCOPY_PFX" || return 1
chmod 600 "$DEPLOY_LOCALCOPY_PFX"
fi
if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then
_err "Failed to copy PFX, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_PFX "$DEPLOY_LOCALCOPY_PFX"
fi
_reload=$DEPLOY_LOCALCOPY_RELOADCMD
_debug "Running reloadcmd $_reload"
if [ -z "$_reload" ]; then
_info "Reloadcmd not provided, skipping."
else
_info "Reloading"
if eval "$_reload"; then
_info "Reload successful."
_savedeployconf DEPLOY_LOCALCOPY_RELOADCMD "$DEPLOY_LOCALCOPY_RELOADCMD" "base64"
else
_err "Reload failed."
return 1
fi
fi
_info "$(__green "'localcopy' deploy success")"
return 0
}
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to mailcow.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
mailcow_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_MAILCOW_PATH
_getdeployconf DEPLOY_MAILCOW_RELOAD
_debug DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
_debug DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
if [ -z "$DEPLOY_MAILCOW_PATH" ]; then
_err "Mailcow path is not found, please define DEPLOY_MAILCOW_PATH."
return 1
fi
_savedeployconf DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
[ -n "$DEPLOY_MAILCOW_RELOAD" ] && _savedeployconf DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
_ssl_path="$DEPLOY_MAILCOW_PATH"
if [ -f "$DEPLOY_MAILCOW_PATH/generate_config.sh" ]; then
_ssl_path="$DEPLOY_MAILCOW_PATH/data/assets/ssl/"
fi
if [ ! -d "$_ssl_path" ]; then
_err "Cannot find mailcow ssl path: $_ssl_path"
return 1
fi
_info "Copying key and cert"
_real_key="$_ssl_path/key.pem"
if ! cat "$_ckey" >"$_real_key"; then
_err "Error: write key file to: $_real_key"
return 1
fi
_real_fullchain="$_ssl_path/cert.pem"
if ! cat "$_cfullchain" >"$_real_fullchain"; then
_err "Error: write cert file to: $_real_fullchain"
return 1
fi
DEFAULT_MAILCOW_RELOAD="docker restart \$(docker ps --quiet --filter name=nginx-mailcow --filter name=dovecot-mailcow --filter name=postfix-mailcow)"
_reload="${DEPLOY_MAILCOW_RELOAD:-$DEFAULT_MAILCOW_RELOAD}"
_info "Run reload: $_reload"
if eval "$_reload"; then
_info "Reload success!"
fi
return 0
}
-287
View File
@@ -1,287 +0,0 @@
#!/usr/bin/env sh
################################################################################
# ACME.sh 3rd party deploy plugin for multiple (same) services
################################################################################
# Authors: tomo2403 (creator), https://github.com/tomo2403
# Updated: 2025-03-01
# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403
################################################################################
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
# A name without a leading '/' is looked up in the certificate directory
# of the domain. An absolute path is used as is, so a single deploy file
# can be shared by all domains, e.g.
# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
################################################################################
# Dependencies:
# - yq
################################################################################
# Return value:
# 0 means success, otherwise error.
################################################################################
MULTIDEPLOY_VERSION="1.0"
# Description: This function handles the deployment of certificates to multiple services.
# It processes the provided certificate files and deploys them according to the
# configuration specified in the multideploy file.
#
# Parameters:
# _cdomain - The domain name for which the certificate is issued.
# _ckey - The private key file for the certificate.
# _ccert - The certificate file.
# _cca - The CA (Certificate Authority) file.
# _cfullchain - The full chain certificate file.
# _cpfx - The PFX (Personal Information Exchange) file.
multideploy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf MULTIDEPLOY_FILENAME
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
else
_savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
_debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
fi
if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then
_err "Failed to preprocess deploy file."
return 1
fi
_debug3 "File" "$file"
# Deploy to services
_deploy_services "$file"
_exitCode="$?"
return "$_exitCode"
}
# Description:
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
# $@ - Posible deploy file names. A name starting with '/' is treated as an
# absolute path, any other name is relative to the domain directory.
# Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() {
# Check if yq is installed
if ! command -v yq >/dev/null 2>&1; then
_err "yq is not installed! Please install yq and try again."
return 1
fi
_debug3 "yq is installed."
# Check if deploy file exists
found_file=""
for file in "$@"; do
if _startswith "$file" "/"; then
_multideploy_path="$file"
else
_multideploy_path="$DOMAIN_PATH/$file"
fi
_debug3 "Checking file" "$_multideploy_path"
if [ -f "$_multideploy_path" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
found_file="$_multideploy_path"
else
_debug3 "File not found"
fi
done
if [ -z "$found_file" ]; then
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one."
return 1
fi
if ! _check_deployfile "$found_file"; then
_err "Deploy file is not valid: $found_file"
return 1
fi
echo "$found_file"
}
# Description:
# This function checks the deploy file for version compatibility and the existence of the specified configuration and services.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The name of the deploy configuration to use.
# Usage:
# _check_deployfile "<deploy_file_path>"
_check_deployfile() {
_deploy_file="$1"
_debug2 "check: Deploy file" "$_deploy_file"
# Check version
_deploy_file_version=$(yq -r '.version' "$_deploy_file")
if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then
_err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version."
return 1
fi
_debug2 "check: Deploy file version is compatible: $_deploy_file_version"
# Extract all services from config
_services=$(yq -r '.services[].name' "$_deploy_file")
if [ -z "$_services" ]; then
_err "Config does not have any services to deploy to."
return 1
fi
_debug2 "check: Config has services."
echo "$_services" | while read -r _service; do
_debug3 " - $_service"
done
# Check if extracted services exist in services list
echo "$_services" | while read -r _service; do
_debug2 "check: Checking service: $_service"
# Check if service exists
_service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file")
if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then
_err "Service '$_service' not found."
return 1
fi
_service_hook=$(echo "$_service_config" | yq -r ".hook" -)
if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then
_err "Service '$_service' does not have a hook."
return 1
fi
_service_environment=$(echo "$_service_config" | yq -r ".environment" -)
if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then
_err "Service '$_service' does not have an environment."
return 1
fi
done
}
# Description: This function takes a list of environment variables in YAML format,
# parses them, and exports each key-value pair as environment variables.
# Arguments:
# $1 - A string containing the list of environment variables in YAML format.
# Usage:
# _export_envs "$env_list"
_export_envs() {
_env_list="$1"
_secure_debug3 "Exporting envs" "$_env_list"
echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do
# Using eval to expand nested variables in the configuration file
_value=$(eval 'echo "'"$_value"'"')
_savedeployconf "$_key" "$_value"
_secure_debug3 "Saved $_key" "$_value"
done
}
# Description:
# This function takes a YAML formatted string of environment variables, parses it,
# and clears each environment variable. It logs the process of clearing each variable.
#
# Note: Environment variables for a hook may be optional and differ between
# services using the same hook.
# If one service sets optional environment variables and another does not, the
# variables may persist and affect subsequent deployments.
# Clearing these variables after each service ensures that only the
# environment variables explicitly specified for each service in the deploy
# file are used.
# Arguments:
# $1 - A YAML formatted string containing environment variable key-value pairs.
# Usage:
# _clear_envs "<yaml_string>"
_clear_envs() {
_env_list="$1"
_secure_debug3 "Clearing envs" "$_env_list"
env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value')
echo "$env_pairs" | while IFS='=' read -r _key _value; do
_debug3 "Deleting key" "$_key"
_cleardeployconf "$_key"
unset -v "$_key"
done
}
# Description:
# This function deploys services listed in the deploy configuration file.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The list of services to deploy.
# Usage:
# _deploy_services "<deploy_file_path>" "<services_list>"
_deploy_services() {
_deploy_file="$1"
_debug3 "Deploy file" "$_deploy_file"
_tempfile=$(mktemp)
trap 'rm -f $_tempfile' EXIT
yq -r '.services[].name' "$_deploy_file" >"$_tempfile"
_debug3 "Services" "$(cat "$_tempfile")"
_failedServices=""
_failedCount=0
while read -r _service <&3; do
_debug2 "Service" "$_service"
_hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file")
_envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file")
_export_envs "$_envs"
if ! _deploy_service "$_service" "$_hook"; then
_failedServices="$_service, $_failedServices"
_failedCount=$((_failedCount + 1))
fi
_clear_envs "$_envs"
done 3<"$_tempfile"
_debug3 "Failed services" "$_failedServices"
_debug2 "Failed count" "$_failedCount"
if [ -n "$_failedServices" ]; then
_info "$(__red "Deployment failed") for services: $_failedServices"
else
_debug "All services deployed successfully."
fi
return "$_failedCount"
}
# Description: Deploys a service using the specified hook.
# Arguments:
# $1 - The name of the service to deploy.
# $2 - The hook to use for deployment.
# Usage:
# _deploy_service <service_name> <hook>
_deploy_service() {
_name="$1"
_hook="$2"
_debug2 "SERVICE" "$_name"
_debug2 "HOOK" "$_hook"
_info "$(__green "Deploying") to '$_name' using '$_hook'"
_deploy "$_cdomain" "$_hook"
}
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env sh
#Here is a sample custom api script.
#This file name is "myapi.sh"
#So, here must be a method myapi_deploy()
#Which will be called by acme.sh to deploy the cert
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
myapi_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "Not implemented yet"
return 1
}
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env sh
# MyDevil.net API (2019-02-03)
#
# MyDevil.net already supports automatic Let's Encrypt certificates,
# except for wildcard domains.
#
# This script depends on `devil` command that MyDevil.net provides,
# which means that it works only on server side.
#
# Author: Marcin Konicki <https://ahwayakchih.neoni.net>
#
######## Public functions #####################
# Usage: mydevil_deploy domain keyfile certfile cafile fullchain
mydevil_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
ip=""
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists "devil"; then
_err "Could not find 'devil' command."
return 1
fi
ip=$(mydevil_get_ip "$_cdomain")
if [ -z "$ip" ]; then
_err "Could not find IP for domain $_cdomain."
return 1
fi
# Delete old certificate first
_info "Removing old certificate for $_cdomain at $ip"
devil ssl www del "$ip" "$_cdomain"
# Add new certificate
_info "Adding new certificate for $_cdomain at $ip"
devil ssl www add "$ip" "$_cfullchain" "$_ckey" "$_cdomain" || return 1
return 0
}
#################### Private functions below ##################################
# Usage: ip=$(mydevil_get_ip domain.com)
# echo $ip
mydevil_get_ip() {
# tr squeezes runs of blanks into one tab so plain cut works everywhere;
# cut -w is BSD-only and unknown to GNU coreutils
devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1
return 0
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to mysqld server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
mysqld_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "deploy cert to mysqld server, Not implemented yet"
return 1
}
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificate to Netlify
# https://docs.netlify.com/api/get-started/#authentication
# https://open-api.netlify.com/#tag/sniCertificate
# This deployment required following variables
# export Netlify_ACCESS_TOKEN="Your Netlify Access Token"
# export Netlify_SITE_ID="Your Netlify Site ID"
# If have more than one SITE ID
# export Netlify_SITE_ID="SITE_ID_1 SITE_ID_2"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
netlify_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$Netlify_ACCESS_TOKEN" ]; then
_err "Netlify_ACCESS_TOKEN is not defined."
return 1
else
_savedomainconf Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
fi
if [ -z "$Netlify_SITE_ID" ]; then
_err "Netlify_SITE_ID is not defined."
return 1
else
_savedomainconf Netlify_SITE_ID "$Netlify_SITE_ID"
fi
_info "Deploying certificate to Netlify..."
## upload certificate
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
for SITE_ID in $Netlify_SITE_ID; do
_request_body="{\"certificate\":\"$string_ccert\",\"key\":\"$string_key\",\"ca_certificates\":\"$string_cca\"}"
_debug _request_body "$_request_body"
_debug Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
export _H1="Authorization: Bearer $Netlify_ACCESS_TOKEN"
_response=$(_post "$_request_body" "https://api.netlify.com/api/v1/sites/$SITE_ID/ssl" "" "POST" "application/json")
if _contains "$_response" "\"error\""; then
_err "Error in deploying $_cdomain certificate to Netlify SITE_ID $SITE_ID."
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Domain $_cdomain certificate successfully deployed to Netlify SITE_ID $SITE_ID."
done
return 0
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to nginx server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
nginx_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "deploy cert to nginx server, Not implemented yet"
return 1
}
-156
View File
@@ -1,156 +0,0 @@
#!/usr/bin/env sh
# This deploy hook is tested on OpenMediaVault 5.x. It supports both local and remote deployment.
# The way it works is that if a cert with the matching domain name is not found, it will firstly create a dummy cert to get its uuid, and then replace it with your cert.
#
# DEPLOY_OMV_WEBUI_ADMIN - This is OMV web gui admin account. Default value is admin. It's required as the user parameter (-u) for the omv-rpc command.
# DEPLOY_OMV_HOST and DEPLOY_OMV_SSH_USER are optional. They are used for remote deployment through ssh (support public key authentication only). Per design, OMV web gui admin doesn't have ssh permission, so another account is needed for ssh.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
openmediavault_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_OMV_WEBUI_ADMIN
if [ -z "$DEPLOY_OMV_WEBUI_ADMIN" ]; then
DEPLOY_OMV_WEBUI_ADMIN="admin"
fi
_savedeployconf DEPLOY_OMV_WEBUI_ADMIN "$DEPLOY_OMV_WEBUI_ADMIN"
_getdeployconf DEPLOY_OMV_HOST
_getdeployconf DEPLOY_OMV_SSH_USER
if [ -n "$DEPLOY_OMV_HOST" ] && [ -n "$DEPLOY_OMV_SSH_USER" ]; then
_info "[OMV deploy-hook] Deploy certificate remotely through ssh."
_savedeployconf DEPLOY_OMV_HOST "$DEPLOY_OMV_HOST"
_savedeployconf DEPLOY_OMV_SSH_USER "$DEPLOY_OMV_SSH_USER"
else
_info "[OMV deploy-hook] Deploy certificate locally."
fi
if [ -n "$DEPLOY_OMV_HOST" ] && [ -n "$DEPLOY_OMV_SSH_USER" ]; then
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'getList' '{\"start\": 0, \"limit\": -1}' | jq -r '.data[] | select(.name==\"/CN='$_cdomain'\") | .uuid'"
# shellcheck disable=SC2029
_uuid=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
if [ -z "$_uuid" ]; then
_info "[OMV deploy-hook] Domain $_cdomain has no certificate in openmediavault, creating it!"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'create' '{\"cn\": \"test.example.com\", \"size\": 4096, \"days\": 3650, \"c\": \"\", \"st\": \"\", \"l\": \"\", \"o\": \"\", \"ou\": \"\", \"email\": \"\"}' | jq -r '.uuid'"
# shellcheck disable=SC2029
_uuid=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
if [ -z "$_uuid" ]; then
_err "[OMV deploy-hook] An error occured while creating the certificate"
return 1
fi
fi
_info "[OMV deploy-hook] Domain $_cdomain has uuid: $_uuid"
_fullchain=$(jq <"$_cfullchain" -aRs .)
_key=$(jq <"$_ckey" -aRs .)
_debug _fullchain "$_fullchain"
_debug _key "$_key"
_info "[OMV deploy-hook] Updating key and certificate in openmediavault"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'set' '{\"uuid\":\"$_uuid\", \"certificate\":$_fullchain, \"privatekey\":$_key, \"comment\":\"acme.sh deployed $(date)\"}'"
# shellcheck disable=SC2029
_result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
_debug _result "$_result"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'setSettings' \$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'getSettings' | jq -c '.sslcertificateref=\"$_uuid\"')"
# shellcheck disable=SC2029
_result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
_debug _result "$_result"
_info "[OMV deploy-hook] Asking openmediavault to apply changes... (this could take some time, hang in there)"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'Config' 'applyChanges' '{\"modules\":[], \"force\": false}'"
# shellcheck disable=SC2029
_result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
_debug _result "$_result"
_info "[OMV deploy-hook] Asking nginx to reload"
_command="nginx -s reload"
# shellcheck disable=SC2029
_result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command")
_debug _command "$_command"
_debug _result "$_result"
else
# shellcheck disable=SC2086
_uuid=$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'getList' '{"start": 0, "limit": -1}' | jq -r '.data[] | select(.name=="/CN='$_cdomain'") | .uuid')
if [ -z "$_uuid" ]; then
_info "[OMV deploy-hook] Domain $_cdomain has no certificate in openmediavault, creating it!"
# shellcheck disable=SC2086
_uuid=$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'create' '{"cn": "test.example.com", "size": 4096, "days": 3650, "c": "", "st": "", "l": "", "o": "", "ou": "", "email": ""}' | jq -r '.uuid')
if [ -z "$_uuid" ]; then
_err "[OMB deploy-hook] An error occured while creating the certificate"
return 1
fi
fi
_info "[OMV deploy-hook] Domain $_cdomain has uuid: $_uuid"
_fullchain=$(jq <"$_cfullchain" -aRs .)
_key=$(jq <"$_ckey" -aRs .)
_debug _fullchain "$_fullchain"
_debug _key "$_key"
_info "[OMV deploy-hook] Updating key and certificate in openmediavault"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'set' '{\"uuid\":\"$_uuid\", \"certificate\":$_fullchain, \"privatekey\":$_key, \"comment\":\"acme.sh deployed $(date)\"}'"
_result=$(eval "$_command")
_debug _command "$_command"
_debug _result "$_result"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'setSettings' \$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'getSettings' | jq -c '.sslcertificateref=\"$_uuid\"')"
_result=$(eval "$_command")
_debug _command "$_command"
_debug _result "$_result"
_info "[OMV deploy-hook] Asking openmediavault to apply changes... (this could take some time, hang in there)"
_command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'Config' 'applyChanges' '{\"modules\":[], \"force\": false}'"
_result=$(eval "$_command")
_debug _command "$_command"
_debug _result "$_result"
_info "[OMV deploy-hook] Asking nginx to reload"
_command="nginx -s reload"
_result=$(eval "$_command")
_debug _command "$_command"
_debug _result "$_result"
fi
return 0
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to opensshd server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
opensshd_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "deploy cert to opensshd server, Not implemented yet"
return 1
}
-262
View File
@@ -1,262 +0,0 @@
#!/usr/bin/env sh
# OpenStack Barbican deploy hook
#
# This requires you to have OpenStackClient and python-barbicanclient
# installed.
#
# You will require Keystone V3 credentials loaded into your environment, which
# could be either password or v3applicationcredential type.
#
# Author: Andy Botting <andy@andybotting.com>
openstack_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists openstack; then
_err "OpenStack client not found"
return 1
fi
_openstack_credentials || return $?
_info "Generate import pkcs12"
_import_pkcs12="$(_mktemp)"
if ! _openstack_to_pkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca"; then
_err "Error creating pkcs12 certificate"
return 1
fi
_debug _import_pkcs12 "$_import_pkcs12"
_base64_pkcs12=$(_base64 "multiline" <"$_import_pkcs12")
secretHrefs=$(_openstack_get_secrets)
_debug secretHrefs "$secretHrefs"
_openstack_store_secret || return $?
if [ -n "$secretHrefs" ]; then
_info "Cleaning up existing secret"
_openstack_delete_secrets || return $?
fi
_info "Certificate successfully deployed"
return 0
}
_openstack_store_secret() {
if ! openstack secret store --name "$_cdomain." -t 'application/octet-stream' -e base64 --payload "$_base64_pkcs12"; then
_err "Failed to create OpenStack secret"
return 1
fi
return
}
_openstack_delete_secrets() {
echo "$secretHrefs" | while read -r secretHref; do
_info "Deleting old secret $secretHref"
if ! openstack secret delete "$secretHref"; then
_err "Failed to delete OpenStack secret"
return 1
fi
done
return
}
_openstack_get_secrets() {
if ! secretHrefs=$(openstack secret list -f value --name "$_cdomain." | cut -d' ' -f1); then
_err "Failed to list secrets"
return 1
fi
echo "$secretHrefs"
}
_openstack_to_pkcs() {
# The existing _toPkcs command can't allow an empty password, due to sh
# -z test, so copied here and forcing the empty password.
_cpfx="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca" -password "pass:"
}
_openstack_credentials() {
_debug "Check OpenStack credentials"
# If we have OS_AUTH_URL already set in the environment, then assume we want
# to use those, otherwise use stored credentials
if [ -n "$OS_AUTH_URL" ]; then
_debug "OS_AUTH_URL env var found, using environment"
else
_debug "OS_AUTH_URL not found, loading stored credentials"
OS_AUTH_URL="${OS_AUTH_URL:-$(_readaccountconf_mutable OS_AUTH_URL)}"
OS_IDENTITY_API_VERSION="${OS_IDENTITY_API_VERSION:-$(_readaccountconf_mutable OS_IDENTITY_API_VERSION)}"
OS_AUTH_TYPE="${OS_AUTH_TYPE:-$(_readaccountconf_mutable OS_AUTH_TYPE)}"
OS_APPLICATION_CREDENTIAL_ID="${OS_APPLICATION_CREDENTIAL_ID:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID)}"
OS_APPLICATION_CREDENTIAL_SECRET="${OS_APPLICATION_CREDENTIAL_SECRET:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET)}"
OS_USERNAME="${OS_USERNAME:-$(_readaccountconf_mutable OS_USERNAME)}"
OS_PASSWORD="${OS_PASSWORD:-$(_readaccountconf_mutable OS_PASSWORD)}"
OS_PROJECT_NAME="${OS_PROJECT_NAME:-$(_readaccountconf_mutable OS_PROJECT_NAME)}"
OS_PROJECT_ID="${OS_PROJECT_ID:-$(_readaccountconf_mutable OS_PROJECT_ID)}"
OS_USER_DOMAIN_NAME="${OS_USER_DOMAIN_NAME:-$(_readaccountconf_mutable OS_USER_DOMAIN_NAME)}"
OS_USER_DOMAIN_ID="${OS_USER_DOMAIN_ID:-$(_readaccountconf_mutable OS_USER_DOMAIN_ID)}"
OS_PROJECT_DOMAIN_NAME="${OS_PROJECT_DOMAIN_NAME:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_NAME)}"
OS_PROJECT_DOMAIN_ID="${OS_PROJECT_DOMAIN_ID:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_ID)}"
fi
# Check each var and either save or clear it depending on whether its set.
# The helps us clear out old vars in the case where a user may want
# to switch between password and app creds
_debug "OS_AUTH_URL" "$OS_AUTH_URL"
if [ -n "$OS_AUTH_URL" ]; then
export OS_AUTH_URL
_saveaccountconf_mutable OS_AUTH_URL "$OS_AUTH_URL"
else
unset OS_AUTH_URL
_clearaccountconf SAVED_OS_AUTH_URL
fi
_debug "OS_IDENTITY_API_VERSION" "$OS_IDENTITY_API_VERSION"
if [ -n "$OS_IDENTITY_API_VERSION" ]; then
export OS_IDENTITY_API_VERSION
_saveaccountconf_mutable OS_IDENTITY_API_VERSION "$OS_IDENTITY_API_VERSION"
else
unset OS_IDENTITY_API_VERSION
_clearaccountconf SAVED_OS_IDENTITY_API_VERSION
fi
_debug "OS_AUTH_TYPE" "$OS_AUTH_TYPE"
if [ -n "$OS_AUTH_TYPE" ]; then
export OS_AUTH_TYPE
_saveaccountconf_mutable OS_AUTH_TYPE "$OS_AUTH_TYPE"
else
unset OS_AUTH_TYPE
_clearaccountconf SAVED_OS_AUTH_TYPE
fi
_debug "OS_APPLICATION_CREDENTIAL_ID" "$OS_APPLICATION_CREDENTIAL_ID"
if [ -n "$OS_APPLICATION_CREDENTIAL_ID" ]; then
export OS_APPLICATION_CREDENTIAL_ID
_saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID "$OS_APPLICATION_CREDENTIAL_ID"
else
unset OS_APPLICATION_CREDENTIAL_ID
_clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_ID
fi
_secure_debug "OS_APPLICATION_CREDENTIAL_SECRET" "$OS_APPLICATION_CREDENTIAL_SECRET"
if [ -n "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then
export OS_APPLICATION_CREDENTIAL_SECRET
_saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET "$OS_APPLICATION_CREDENTIAL_SECRET"
else
unset OS_APPLICATION_CREDENTIAL_SECRET
_clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_SECRET
fi
_debug "OS_USERNAME" "$OS_USERNAME"
if [ -n "$OS_USERNAME" ]; then
export OS_USERNAME
_saveaccountconf_mutable OS_USERNAME "$OS_USERNAME"
else
unset OS_USERNAME
_clearaccountconf SAVED_OS_USERNAME
fi
_secure_debug "OS_PASSWORD" "$OS_PASSWORD"
if [ -n "$OS_PASSWORD" ]; then
export OS_PASSWORD
_saveaccountconf_mutable OS_PASSWORD "$OS_PASSWORD"
else
unset OS_PASSWORD
_clearaccountconf SAVED_OS_PASSWORD
fi
_debug "OS_PROJECT_NAME" "$OS_PROJECT_NAME"
if [ -n "$OS_PROJECT_NAME" ]; then
export OS_PROJECT_NAME
_saveaccountconf_mutable OS_PROJECT_NAME "$OS_PROJECT_NAME"
else
unset OS_PROJECT_NAME
_clearaccountconf SAVED_OS_PROJECT_NAME
fi
_debug "OS_PROJECT_ID" "$OS_PROJECT_ID"
if [ -n "$OS_PROJECT_ID" ]; then
export OS_PROJECT_ID
_saveaccountconf_mutable OS_PROJECT_ID "$OS_PROJECT_ID"
else
unset OS_PROJECT_ID
_clearaccountconf SAVED_OS_PROJECT_ID
fi
_debug "OS_USER_DOMAIN_NAME" "$OS_USER_DOMAIN_NAME"
if [ -n "$OS_USER_DOMAIN_NAME" ]; then
export OS_USER_DOMAIN_NAME
_saveaccountconf_mutable OS_USER_DOMAIN_NAME "$OS_USER_DOMAIN_NAME"
else
unset OS_USER_DOMAIN_NAME
_clearaccountconf SAVED_OS_USER_DOMAIN_NAME
fi
_debug "OS_USER_DOMAIN_ID" "$OS_USER_DOMAIN_ID"
if [ -n "$OS_USER_DOMAIN_ID" ]; then
export OS_USER_DOMAIN_ID
_saveaccountconf_mutable OS_USER_DOMAIN_ID "$OS_USER_DOMAIN_ID"
else
unset OS_USER_DOMAIN_ID
_clearaccountconf SAVED_OS_USER_DOMAIN_ID
fi
_debug "OS_PROJECT_DOMAIN_NAME" "$OS_PROJECT_DOMAIN_NAME"
if [ -n "$OS_PROJECT_DOMAIN_NAME" ]; then
export OS_PROJECT_DOMAIN_NAME
_saveaccountconf_mutable OS_PROJECT_DOMAIN_NAME "$OS_PROJECT_DOMAIN_NAME"
else
unset OS_PROJECT_DOMAIN_NAME
_clearaccountconf SAVED_OS_PROJECT_DOMAIN_NAME
fi
_debug "OS_PROJECT_DOMAIN_ID" "$OS_PROJECT_DOMAIN_ID"
if [ -n "$OS_PROJECT_DOMAIN_ID" ]; then
export OS_PROJECT_DOMAIN_ID
_saveaccountconf_mutable OS_PROJECT_DOMAIN_ID "$OS_PROJECT_DOMAIN_ID"
else
unset OS_PROJECT_DOMAIN_ID
_clearaccountconf SAVED_OS_PROJECT_DOMAIN_ID
fi
if [ "$OS_AUTH_TYPE" = "v3applicationcredential" ]; then
# Application Credential auth
if [ -z "$OS_APPLICATION_CREDENTIAL_ID" ] || [ -z "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then
_err "When using OpenStack application credentials, OS_APPLICATION_CREDENTIAL_ID"
_err "and OS_APPLICATION_CREDENTIAL_SECRET must be set."
_err "Please check your credentials and try again."
return 1
fi
else
# Password auth
if [ -z "$OS_USERNAME" ] || [ -z "$OS_PASSWORD" ]; then
_err "OpenStack username or password not found."
_err "Please check your credentials and try again."
return 1
fi
if [ -z "$OS_PROJECT_NAME" ] && [ -z "$OS_PROJECT_ID" ]; then
_err "When using password authentication, OS_PROJECT_NAME or"
_err "OS_PROJECT_ID must be set."
_err "Please check your credentials and try again."
return 1
fi
fi
return 0
}
-329
View File
@@ -1,329 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificates to Palo Alto Networks PANOS via API
# Note PANOS API KEY and IP address needs to be set prior to running.
# The following variables exported from environment will be used.
# If not set then values previously saved in domain.conf file are used.
#
# Firewall admin with superuser and IP address is required.
#
# REQUIRED:
# export PANOS_HOST=""
# export PANOS_USER="" #User *MUST* have Commit and Import Permissions in XML API for Admin Role
# export PANOS_PASS=""
#
# OPTIONAL
# export PANOS_TEMPLATE="" # Template Name of panorama managed devices
# export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically
# export PANOS_VSYS="Shared" # name of the vsys to import the certificate
# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit
#
# The script will automatically generate a new API key if
# no key is found, or if a saved key has expired or is invalid.
_COMMIT_WAIT_INTERVAL=30 # query commit status every 30 seconds
_COMMIT_WAIT_ITERATIONS=20 # query commit status 20 times (20*30 = 600 seconds = 10 minutes)
# This function is to parse the XML response from the firewall
parse_response() {
type=$2
_debug "API Response: $1"
if [ "$type" = 'keygen' ]; then
status=$(echo "$1" | sed 's/^.*\(['\'']\)\([a-z]*\)'\''.*/\2/g')
if [ "$status" = "success" ]; then
panos_key=$(echo "$1" | sed 's/^.*\(<key>\)\(.*\)<\/key>.*/\2/g')
_panos_key=$panos_key
else
message="PAN-OS Key could not be set."
fi
else
if [ "$type" = 'commit' ]; then
job_id=$(echo "$1" | sed 's/^.*\(<job>\)\(.*\)<\/job>.*/\2/g')
_commit_job_id=$job_id
elif [ "$type" = 'job_status' ]; then
job_status=$(echo "$1" | tr -d '\n' | sed 's/^.*<result>\([^<]*\)<\/result>.*/\1/g')
_commit_job_status=$job_status
fi
status=$(echo "$1" | tr -d '\n' | sed 's/^.*"\([a-z]*\)".*/\1/g')
message=$(echo "$1" | tr -d '\n' | sed 's/.*\(<result>\|<msg>\|<line>\)\([^<]*\).*/\2/g')
_debug "Firewall message: $message"
if [ "$type" = 'keytest' ] && [ "$status" != "success" ]; then
_debug "**** API Key has EXPIRED or is INVALID ****"
unset _panos_key
fi
fi
return 0
}
#This function is used to deploy to the firewall
deployer() {
content=""
type=$1 # Types are keytest, keygen, cert, key, commit, job_status, push
panos_url="https://$_panos_host/api/"
export _H1="Content-Type: application/x-www-form-urlencoded"
#Test API Key by performing a lookup
if [ "$type" = 'keytest' ]; then
_debug "**** Testing saved API Key ****"
# Get Version Info to test key
content="type=version&key=$_panos_key"
## Exclude all scopes for the empty commit
#_exclude_scope="<device-and-network>excluded</device-and-network><shared-object>excluded</shared-object>"
#content="type=commit&action=partial&key=$_panos_key&cmd=<commit><partial>$_exclude_scope<admin><member>$_panos_user</member></admin></partial></commit>"
fi
# Generate API Key
if [ "$type" = 'keygen' ]; then
_debug "**** Generating new API Key ****"
content="type=keygen&user=$_panos_user&password=$_panos_pass"
# content="$content${nl}--$delim${nl}Content-Disposition: form-data; type=\"keygen\"; user=\"$_panos_user\"; password=\"$_panos_pass\"${nl}Content-Type: application/octet-stream${nl}${nl}"
fi
# Deploy Cert or Key
if [ "$type" = 'cert' ] || [ "$type" = 'key' ]; then
_debug "**** Deploying $type ****"
#Generate DELIM
delim="-----MultipartDelimiter$(date "+%s%N")"
nl="\015\012"
#Set Header
export _H1="Content-Type: multipart/form-data; boundary=$delim"
if [ "$type" = 'cert' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi
if [ "$_panos_vsys" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
fi
fi
if [ "$type" = 'key' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi
if [ "$_panos_vsys" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
fi
fi
#Close multipart
content="$content${nl}--$delim--${nl}${nl}"
#Convert CRLF
content=$(printf %b "$content")
fi
# Commit changes
if [ "$type" = 'commit' ]; then
_debug "**** Committing changes ****"
#Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution!
if [ "$FORCE" ]; then
_debug "Force switch detected. Committing ALL changes to the firewall."
cmd=$(printf "%s" "<commit><force><partial><admin><member>$_panos_user</member></admin></partial></force></commit>" | _url_encode)
else
cmd=$(printf "%s" "<commit><partial><admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
fi
content="type=commit&action=partial&key=$_panos_key&cmd=$cmd"
fi
# Query job status
if [ "$type" = 'job_status' ]; then
echo "**** Querying job $_commit_job_id status ****"
cmd=$(printf "%s" "<show><jobs><id>$_commit_job_id</id></jobs></show>" | _url_encode)
content="type=op&key=$_panos_key&cmd=$cmd"
fi
# Push changes
if [ "$type" = 'push' ]; then
echo "**** Pushing changes ****"
cmd=$(printf "%s" "<commit-all><template-stack><name>$_panos_template_stack</name><admin><member>$_panos_user</member></admin></template-stack></commit-all>" | _url_encode)
content="type=commit&action=all&key=$_panos_key&cmd=$cmd"
fi
response=$(_post "$content" "$panos_url" "" "POST")
parse_response "$response" "$type"
# Saving response to variables
response_status=$status
_debug response_status "$response_status"
if [ "$response_status" = "success" ]; then
_debug "Successfully deployed $type"
return 0
elif [ "$_commit_job_status" ]; then
_debug "Commit Job Status = $_commit_job_status"
else
_err "Deploy of type $type failed. Try deploying with --debug to troubleshoot."
_debug "$message"
return 1
fi
}
# This is the main function that will call the other functions to deploy everything.
panos_deploy() {
_cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename
_ckey="$2"
_cfullchain="$5"
# VALID FILE CHECK
if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
_err "Unable to find a valid key and/or cert. If this is an ECDSA/ECC cert, use the --ecc flag when deploying."
return 1
fi
# PANOS_HOST
if [ "$PANOS_HOST" ]; then
_debug "Detected ENV variable PANOS_HOST. Saving to file."
_savedeployconf PANOS_HOST "$PANOS_HOST" 1
else
_debug "Attempting to load variable PANOS_HOST from file."
_getdeployconf PANOS_HOST
fi
# PANOS USER
if [ "$PANOS_USER" ]; then
_debug "Detected ENV variable PANOS_USER. Saving to file."
_savedeployconf PANOS_USER "$PANOS_USER" 1
else
_debug "Attempting to load variable PANOS_USER from file."
_getdeployconf PANOS_USER
fi
# PANOS_PASS
if [ "$PANOS_PASS" ]; then
_debug "Detected ENV variable PANOS_PASS. Saving to file."
_savedeployconf PANOS_PASS "$PANOS_PASS" 1
else
_debug "Attempting to load variable PANOS_PASS from file."
_getdeployconf PANOS_PASS
fi
# PANOS_KEY
if [ "$PANOS_KEY" ]; then
_debug "Detected ENV variable PANOS_KEY. Saving to file."
_savedeployconf PANOS_KEY "$PANOS_KEY" 1
else
_debug "Attempting to load variable PANOS_KEY from file."
_getdeployconf PANOS_KEY
fi
# PANOS_TEMPLATE
if [ "$PANOS_TEMPLATE" ]; then
_debug "Detected ENV variable PANOS_TEMPLATE. Saving to file."
_savedeployconf PANOS_TEMPLATE "$PANOS_TEMPLATE" 1
else
_debug "Attempting to load variable PANOS_TEMPLATE from file."
_getdeployconf PANOS_TEMPLATE
fi
# PANOS_TEMPLATE_STACK
if [ "$PANOS_TEMPLATE_STACK" ]; then
_debug "Detected ENV variable PANOS_TEMPLATE_STACK. Saving to file."
_savedeployconf PANOS_TEMPLATE_STACK "$PANOS_TEMPLATE_STACK" 1
else
_debug "Attempting to load variable PANOS_TEMPLATE_STACK from file."
_getdeployconf PANOS_TEMPLATE_STACK
fi
# PANOS_TEMPLATE_STACK
if [ "$PANOS_VSYS" ]; then
_debug "Detected ENV variable PANOS_VSYS. Saving to file."
_savedeployconf PANOS_VSYS "$PANOS_VSYS" 1
else
_debug "Attempting to load variable PANOS_VSYS from file."
_getdeployconf PANOS_VSYS
fi
# PANOS_CERTNAME
if [ "$PANOS_CERTNAME" ]; then
_debug "Detected ENV variable PANOS_CERTNAME. Saving to file."
_savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1
else
_debug "Attempting to load variable PANOS_CERTNAME from file."
_getdeployconf PANOS_CERTNAME
fi
#Store variables
_panos_host=$PANOS_HOST
_panos_user=$PANOS_USER
_panos_pass=$PANOS_PASS
_panos_key=$PANOS_KEY
_panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS
_panos_certname=$PANOS_CERTNAME
#Test API Key if found. If the key is invalid, the variable _panos_key will be unset.
if [ "$_panos_host" ] && [ "$_panos_key" ]; then
_debug "**** Testing API KEY ****"
deployer keytest
fi
# Check for valid variables
if [ -z "$_panos_host" ]; then
_err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
else
# Use certificate name based on the first domain on the certificate if no custom certificate name is set
if [ -z "$_panos_certname" ]; then
_panos_certname="$_cdomain"
_savedeployconf PANOS_CERTNAME "$_panos_certname" 1
fi
# Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then
if [ -z "$_panos_user" ]; then
_err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_pass" ]; then
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
fi
_debug "**** Generating new PANOS API KEY ****"
deployer keygen
_savedeployconf PANOS_KEY "$_panos_key" 1
fi
# Confirm that a valid key was generated
if [ -z "$_panos_key" ]; then
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
return 1
else
# A commit of a failed import would leave a mismatched cert/key pair
# on the firewall and can lock the admin out of the management
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
if ! deployer cert; then
_err "Cert import failed. Aborting without committing."
return 1
fi
if ! deployer key; then
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
return 1
fi
if ! deployer commit; then
return 1
fi
if [ "$_panos_template_stack" ]; then
# try to get job status for 20 times in 30 sec interval
i=0
while [ "$i" -lt $_COMMIT_WAIT_ITERATIONS ]; do
deployer job_status
if [ "$_commit_job_status" = "OK" ]; then
echo "Commit finished!"
break
fi
sleep $_COMMIT_WAIT_INTERVAL
i=$((i + 1))
done
deployer push
fi
fi
fi
}
-123
View File
@@ -1,123 +0,0 @@
#!/usr/bin/env sh
# Script to deploy cert to Peplink Routers
#
# The following environment variables must be set:
#
# PEPLINK_Hostname - Peplink hostname
# PEPLINK_Username - Peplink username to login
# PEPLINK_Password - Peplink password to login
#
# The following environmental variables may be set if you don't like their
# default values:
#
# PEPLINK_Certtype - Certificate type to target for replacement
# defaults to "webadmin", can be one of:
# * "chub" (ContentHub)
# * "openvpn" (OpenVPN CA)
# * "portal" (Captive Portal SSL)
# * "webadmin" (Web Admin SSL)
# * "webproxy" (Proxy Root CA)
# * "wwan_ca" (Wi-Fi WAN CA)
# * "wwan_client" (Wi-Fi WAN Client)
# PEPLINK_Scheme - defaults to "https"
# PEPLINK_Port - defaults to "443"
#
#returns 0 means success, otherwise error.
######## Public functions #####################
_peplink_get_cookie_data() {
grep -i "\W$1=" | grep -i "^Set-Cookie:" | _tail_n 1 | _egrep_o "$1=[^;]*;" | tr -d ';'
}
#domain keyfile certfile cafile fullchain
peplink_deploy() {
_cdomain="$1"
_ckey="$2"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _cfullchain "$_cfullchain"
_debug _ckey "$_ckey"
# Get Hostname, Username and Password, but don't save until we successfully authenticate
_getdeployconf PEPLINK_Hostname
_getdeployconf PEPLINK_Username
_getdeployconf PEPLINK_Password
if [ -z "${PEPLINK_Hostname:-}" ] || [ -z "${PEPLINK_Username:-}" ] || [ -z "${PEPLINK_Password:-}" ]; then
_err "PEPLINK_Hostname & PEPLINK_Username & PEPLINK_Password must be set"
return 1
fi
_debug2 PEPLINK_Hostname "$PEPLINK_Hostname"
_debug2 PEPLINK_Username "$PEPLINK_Username"
_secure_debug2 PEPLINK_Password "$PEPLINK_Password"
# Optional certificate type, scheme, and port for Peplink
_getdeployconf PEPLINK_Certtype
_getdeployconf PEPLINK_Scheme
_getdeployconf PEPLINK_Port
# Don't save the certificate type until we verify it exists and is supported
_savedeployconf PEPLINK_Scheme "$PEPLINK_Scheme"
_savedeployconf PEPLINK_Port "$PEPLINK_Port"
# Default vaules for certificate type, scheme, and port
[ -n "${PEPLINK_Certtype}" ] || PEPLINK_Certtype="webadmin"
[ -n "${PEPLINK_Scheme}" ] || PEPLINK_Scheme="https"
[ -n "${PEPLINK_Port}" ] || PEPLINK_Port="443"
_debug2 PEPLINK_Certtype "$PEPLINK_Certtype"
_debug2 PEPLINK_Scheme "$PEPLINK_Scheme"
_debug2 PEPLINK_Port "$PEPLINK_Port"
_base_url="$PEPLINK_Scheme://$PEPLINK_Hostname:$PEPLINK_Port"
_debug _base_url "$_base_url"
# Login, get the auth token from the cookie
_info "Logging into $PEPLINK_Hostname:$PEPLINK_Port"
encoded_username="$(printf "%s" "$PEPLINK_Username" | _url_encode)"
encoded_password="$(printf "%s" "$PEPLINK_Password" | _url_encode)"
response=$(_post "func=login&username=$encoded_username&password=$encoded_password" "$_base_url/cgi-bin/MANGA/api.cgi")
auth_token=$(_peplink_get_cookie_data "bauth" <"$HTTP_HEADER")
_debug3 response "$response"
_debug auth_token "$auth_token"
if [ -z "$auth_token" ]; then
_err "Unable to authenticate to $PEPLINK_Hostname:$PEPLINK_Port using $PEPLINK_Scheme."
_err "Check your username and password."
return 1
fi
_H1="Cookie: $auth_token"
export _H1
_debug2 H1 "${_H1}"
# Now that we know the hostnameusername and password are good, save them
_savedeployconf PEPLINK_Hostname "$PEPLINK_Hostname"
_savedeployconf PEPLINK_Username "$PEPLINK_Username"
_savedeployconf PEPLINK_Password "$PEPLINK_Password"
_info "Generate form POST request"
encoded_key="$(_url_encode <"$_ckey")"
encoded_fullchain="$(_url_encode <"$_cfullchain")"
body="cert_type=$PEPLINK_Certtype&cert_uid=&section=CERT_modify&key_pem=$encoded_key&key_pem_passphrase=&key_pem_passphrase_confirm=&cert_pem=$encoded_fullchain"
_debug3 body "$body"
_info "Upload $PEPLINK_Certtype certificate to the Peplink"
response=$(_post "$body" "$_base_url/cgi-bin/MANGA/admin.cgi")
_debug3 response "$response"
if echo "$response" | grep 'Success' >/dev/null; then
# We've verified this certificate type is valid, so save it
_savedeployconf PEPLINK_Certtype "$PEPLINK_Certtype"
_info "Certificate was updated"
return 0
else
_err "Unable to update certificate, error code $response"
return 1
fi
}
-139
View File
@@ -1,139 +0,0 @@
#!/usr/bin/env sh
# Deploy certificates to a proxmox backup server using the API.
#
# Environment variables that can be set are:
# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to
# _cdomain.
# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on.
# Defaults to 8007.
# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root.
# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates
# with. Defaults to pam.
# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the
# user account. Defaults to acme.
# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required.
proxmoxbs_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug2 _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# "Sane" defaults.
_getdeployconf DEPLOY_PROXMOXBS_SERVER
if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then
_target_hostname="$_cdomain"
else
_target_hostname="$DEPLOY_PROXMOXBS_SERVER"
_savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER"
fi
_debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname"
_getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT
if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then
_target_port="8007"
else
_target_port="$DEPLOY_PROXMOXBS_SERVER_PORT"
_savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT"
fi
_debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port"
# Complete URL.
_target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom"
_debug TARGET_URL "$_target_url"
# More "sane" defaults.
_getdeployconf DEPLOY_PROXMOXBS_USER
if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then
_proxmoxbs_user="root"
else
_proxmoxbs_user="$DEPLOY_PROXMOXBS_USER"
_savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER"
fi
_debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user"
_getdeployconf DEPLOY_PROXMOXBS_USER_REALM
if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then
_proxmoxbs_user_realm="pam"
else
_proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM"
_savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM"
fi
_debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm"
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then
_proxmoxbs_api_token_name="acme"
else
_proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
fi
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name"
# This is required.
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then
_err "API key not provided."
return 1
else
_proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
fi
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key"
# PBS API Token header value. Used in "Authorization: PBSAPIToken".
_proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}"
_debug2 "Auth Header" "$_proxmoxbs_header_api_token"
# Ugly. I hate putting heredocs inside functions because heredocs don't
# account for whitespace correctly but it _does_ work and is several times
# cleaner than anything else I had here.
#
# This dumps the json payload to a variable that should be passable to the
# _psot function.
_json_payload=$(
cat <<HEREDOC
{
"certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')",
"key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')",
"node":"localhost",
"restart":true,
"force":true
}
HEREDOC
)
_debug2 Payload "$_json_payload"
_info "Push certificates to server"
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
}
-151
View File
@@ -1,151 +0,0 @@
#!/usr/bin/env sh
# Deploy certificates to a proxmox virtual environment node using the API.
#
# Environment variables that can be set are:
# `DEPLOY_PROXMOXVE_SERVER`: The hostname of the proxmox ve node. Defaults to
# _cdomain.
# `DEPLOY_PROXMOXVE_SERVER_PORT`: The port number the management interface is on.
# Defaults to 8006.
# `DEPLOY_PROXMOXVE_NODE_NAME`: The name of the node we'll be connecting to.
# Defaults to the host portion of the server
# domain name.
# `DEPLOY_PROXMOXVE_USER`: The user we'll connect as. Defaults to root.
# `DEPLOY_PROXMOXVE_USER_REALM`: The authentication realm the user authenticates
# with. Defaults to pam.
# `DEPLOY_PROXMOXVE_API_TOKEN_NAME`: The name of the API token created for the
# user account. Defaults to acme.
# `DEPLOY_PROXMOXVE_API_TOKEN_KEY`: The API token. Required.
proxmoxve_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug2 _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# "Sane" defaults.
_getdeployconf DEPLOY_PROXMOXVE_SERVER
if [ -z "$DEPLOY_PROXMOXVE_SERVER" ]; then
_target_hostname="$_cdomain"
else
_target_hostname="$DEPLOY_PROXMOXVE_SERVER"
_savedeployconf DEPLOY_PROXMOXVE_SERVER "$DEPLOY_PROXMOXVE_SERVER"
fi
_debug2 DEPLOY_PROXMOXVE_SERVER "$_target_hostname"
_getdeployconf DEPLOY_PROXMOXVE_SERVER_PORT
if [ -z "$DEPLOY_PROXMOXVE_SERVER_PORT" ]; then
_target_port="8006"
else
_target_port="$DEPLOY_PROXMOXVE_SERVER_PORT"
_savedeployconf DEPLOY_PROXMOXVE_SERVER_PORT "$DEPLOY_PROXMOXVE_SERVER_PORT"
fi
_debug2 DEPLOY_PROXMOXVE_SERVER_PORT "$_target_port"
_getdeployconf DEPLOY_PROXMOXVE_NODE_NAME
if [ -z "$DEPLOY_PROXMOXVE_NODE_NAME" ]; then
_node_name=$(echo "$_target_hostname" | cut -d. -f1)
else
_node_name="$DEPLOY_PROXMOXVE_NODE_NAME"
_savedeployconf DEPLOY_PROXMOXVE_NODE_NAME "$DEPLOY_PROXMOXVE_NODE_NAME"
fi
_debug2 DEPLOY_PROXMOXVE_NODE_NAME "$_node_name"
# Complete URL.
_target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/${_node_name}/certificates/custom"
_debug TARGET_URL "$_target_url"
# More "sane" defaults.
_getdeployconf DEPLOY_PROXMOXVE_USER
if [ -z "$DEPLOY_PROXMOXVE_USER" ]; then
_proxmoxve_user="root"
else
_proxmoxve_user="$DEPLOY_PROXMOXVE_USER"
_savedeployconf DEPLOY_PROXMOXVE_USER "$DEPLOY_PROXMOXVE_USER"
fi
_debug2 DEPLOY_PROXMOXVE_USER "$_proxmoxve_user"
_getdeployconf DEPLOY_PROXMOXVE_USER_REALM
if [ -z "$DEPLOY_PROXMOXVE_USER_REALM" ]; then
_proxmoxve_user_realm="pam"
else
_proxmoxve_user_realm="$DEPLOY_PROXMOXVE_USER_REALM"
_savedeployconf DEPLOY_PROXMOXVE_USER_REALM "$DEPLOY_PROXMOXVE_USER_REALM"
fi
_debug2 DEPLOY_PROXMOXVE_USER_REALM "$_proxmoxve_user_realm"
_getdeployconf DEPLOY_PROXMOXVE_API_TOKEN_NAME
if [ -z "$DEPLOY_PROXMOXVE_API_TOKEN_NAME" ]; then
_proxmoxve_api_token_name="acme"
else
_proxmoxve_api_token_name="$DEPLOY_PROXMOXVE_API_TOKEN_NAME"
_savedeployconf DEPLOY_PROXMOXVE_API_TOKEN_NAME "$DEPLOY_PROXMOXVE_API_TOKEN_NAME"
fi
_debug2 DEPLOY_PROXMOXVE_API_TOKEN_NAME "$_proxmoxve_api_token_name"
# This is required.
_getdeployconf DEPLOY_PROXMOXVE_API_TOKEN_KEY
if [ -z "$DEPLOY_PROXMOXVE_API_TOKEN_KEY" ]; then
_err "API key not provided."
return 1
else
_proxmoxve_api_token_key="$DEPLOY_PROXMOXVE_API_TOKEN_KEY"
_savedeployconf DEPLOY_PROXMOXVE_API_TOKEN_KEY "$DEPLOY_PROXMOXVE_API_TOKEN_KEY"
fi
_debug2 DEPLOY_PROXMOXVE_API_TOKEN_KEY "$_proxmoxve_api_token_key"
# PVE API Token header value. Used in "Authorization: PVEAPIToken".
_proxmoxve_header_api_token="${_proxmoxve_user}@${_proxmoxve_user_realm}!${_proxmoxve_api_token_name}=${_proxmoxve_api_token_key}"
_debug2 "Auth Header" "$_proxmoxve_header_api_token"
# Ugly. I hate putting heredocs inside functions because heredocs don't
# account for whitespace correctly but it _does_ work and is several times
# cleaner than anything else I had here.
#
# This dumps the json payload to a variable that should be passable to the
# _psot function.
_json_payload=$(
cat <<HEREDOC
{
"certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')",
"key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')",
"node":"$_node_name",
"restart":"1",
"force":"1"
}
HEREDOC
)
_debug2 Payload "$_json_payload"
_info "Push certificates to server"
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
}
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to pureftpd server.
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
pureftpd_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_err "deploy cert to pureftpd server, Not implemented yet"
return 1
}
-104
View File
@@ -1,104 +0,0 @@
#!/usr/bin/env sh
# Script to create certificate to qiniu.com
#
# This deployment required following variables
# export QINIU_AK="QINIUACCESSKEY"
# export QINIU_SK="QINIUSECRETKEY"
# export QINIU_CDN_DOMAIN="cdn.example.com"
# If you have more than one domain, just
# export QINIU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
# Optional: force HTTPS redirect (default: false)
# export QINIU_FORCE_HTTPS="true"
QINIU_API_BASE="https://api.qiniu.com"
qiniu_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$QINIU_AK" ]; then
_err "QINIU_AK is not defined."
return 1
else
_savedomainconf QINIU_AK "$QINIU_AK"
fi
if [ -z "$QINIU_SK" ]; then
_err "QINIU_SK is not defined."
return 1
else
_savedomainconf QINIU_SK "$QINIU_SK"
fi
if [ "$QINIU_CDN_DOMAIN" ]; then
_savedomainconf QINIU_CDN_DOMAIN "$QINIU_CDN_DOMAIN"
else
QINIU_CDN_DOMAIN="$_cdomain"
fi
if [ -z "$QINIU_FORCE_HTTPS" ]; then
QINIU_FORCE_HTTPS="false"
else
_savedomainconf QINIU_FORCE_HTTPS "$QINIU_FORCE_HTTPS"
fi
## upload certificate
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
sslcert_path="/sslcert"
sslcerl_body="{\"name\":\"$_cdomain\",\"common_name\":\"$QINIU_CDN_DOMAIN\",\"ca\":\"$string_fullchain\",\"pri\":\"$string_key\"}"
sslcert_access_token="$(_make_access_token "$sslcert_path")"
_debug sslcert_access_token "$sslcert_access_token"
export _H1="Authorization: QBox $sslcert_access_token"
sslcert_response=$(_post "$sslcerl_body" "$QINIU_API_BASE$sslcert_path" 0 "POST" "application/json" | _dbase64)
if ! _contains "$sslcert_response" "certID"; then
_err "Error in creating certificate:"
_err "$sslcert_response"
return 1
fi
_debug sslcert_response "$sslcert_response"
_info "Certificate successfully uploaded, updating domain $_cdomain"
## extract certId
_certId="$(printf "%s" "$sslcert_response" | _normalizeJson | _egrep_o "certID\": *\"[^\"]*\"" | cut -d : -f 2)"
_debug certId "$_certId"
## update domain ssl config
update_body="{\"certid\":$_certId,\"forceHttps\":$QINIU_FORCE_HTTPS}"
for domain in $QINIU_CDN_DOMAIN; do
update_path="/domain/$domain/httpsconf"
update_access_token="$(_make_access_token "$update_path")"
_debug update_access_token "$update_access_token"
export _H1="Authorization: QBox $update_access_token"
update_response=$(_post "$update_body" "$QINIU_API_BASE$update_path" 0 "PUT" "application/json" | _dbase64)
if _contains "$update_response" "error"; then
_err "Error in updating domain $domain httpsconf:"
_err "$update_response"
return 1
fi
_debug update_response "$update_response"
_info "Domain $domain certificate has been deployed successfully"
done
return 0
}
_make_access_token() {
_token="$(printf "%s\n" "$1" | _hmac "sha1" "$(printf "%s" "$QINIU_SK" | _hex_dump | tr -d " ")" | _base64 | tr -- '+/' '-_')"
echo "$QINIU_AK:$_token"
}
-205
View File
@@ -1,205 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to routeros router.
# Deploy the cert to remote routeros
#
# ```sh
# acme.sh --deploy -d ftp.example.com --deploy-hook routeros
# ```
#
# Before you can deploy the certificate to router os, you need
# to add the id_rsa.pub key to the routeros and assign a user
# to that key.
#
# The user need to have access to ssh, ftp, read and write.
#
# There are no need to enable ftp service for the script to work,
# as they are transmitted over SCP, however ftp is needed to store
# the files on the router.
#
# Then you need to set the environment variables for the
# deploy script to work.
#
# ```sh
# export ROUTER_OS_USERNAME=certuser
# export ROUTER_OS_HOST=router.example.com
# export ROUTER_OS_PORT=22
#
# acme.sh --deploy -d ftp.example.com --deploy-hook routeros
# ```
#
# The deploy script will remove previously deployed certificates,
# and it does this with an assumption on how RouterOS names imported
# certificates, adding a "cer_0" suffix at the end. This is true for
# versions 6.32 -> 6.41.3, but it is not guaranteed that it will be
# true for future versions when upgrading.
#
# If the router have other certificates with the same name as the one
# beeing deployed, then this script will remove those certificates.
#
# At the end of the script, the services that use those certificates
# could be updated. Currently only the www-ssl service is beeing
# updated, but more services could be added.
#
# For instance:
# ```sh
# export ROUTER_OS_ADDITIONAL_SERVICES="/ip service set api-ssl certificate=$_cdomain.cer_0"
# ```
#
# One optional thing to do as well is to create a script that updates
# all the required services and run that script in a single command.
#
# To adopt parameters to `scp` and/or `ssh` set the optional
# `ROUTER_OS_SSH_CMD` and `ROUTER_OS_SCP_CMD` variables accordingly,
# see ssh(1) and scp(1) for parameters to those commands.
#
# Example:
# ```ssh
# export ROUTER_OS_SSH_CMD="ssh -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
# export ROUTER_OS_SCP_CMD="scp -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
# ````
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
routeros_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_err_code=0
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf ROUTER_OS_HOST
if [ -z "$ROUTER_OS_HOST" ]; then
_debug "Using _cdomain as ROUTER_OS_HOST, please set if not correct."
ROUTER_OS_HOST="$_cdomain"
fi
_getdeployconf ROUTER_OS_USERNAME
if [ -z "$ROUTER_OS_USERNAME" ]; then
_err "Need to set the env variable ROUTER_OS_USERNAME"
return 1
fi
_getdeployconf ROUTER_OS_PORT
if [ -z "$ROUTER_OS_PORT" ]; then
_debug "Using default port 22 as ROUTER_OS_PORT, please set if not correct."
ROUTER_OS_PORT=22
fi
_getdeployconf ROUTER_OS_SSH_CMD
if [ -z "$ROUTER_OS_SSH_CMD" ]; then
_debug "Use default ssh setup."
ROUTER_OS_SSH_CMD="ssh -p $ROUTER_OS_PORT"
fi
_getdeployconf ROUTER_OS_SCP_CMD
if [ -z "$ROUTER_OS_SCP_CMD" ]; then
_debug "USe default scp setup."
ROUTER_OS_SCP_CMD="scp -P $ROUTER_OS_PORT"
fi
_getdeployconf ROUTER_OS_ADDITIONAL_SERVICES
if [ -z "$ROUTER_OS_ADDITIONAL_SERVICES" ]; then
_debug "Not enabling additional services"
ROUTER_OS_ADDITIONAL_SERVICES=""
fi
_savedeployconf ROUTER_OS_HOST "$ROUTER_OS_HOST"
_savedeployconf ROUTER_OS_USERNAME "$ROUTER_OS_USERNAME"
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
# push key to routeros
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
return $_err_code
fi
# push certificate chain to routeros
if ! _scp_certificate "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"; then
return $_err_code
fi
DEPLOY_SCRIPT_CMD=":do {/system script remove \"LECertDeploy-$_cdomain\" } on-error={ }; \
/system script add name=\"LECertDeploy-$_cdomain\" owner=$ROUTER_OS_USERNAME \
comment=\"generated by routeros deploy script in acme.sh\" \
source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
\n/certificate remove [ find name=$_cdomain.cer_3 ];\
\ndelay 1;\
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
\ndelay 1;\
\n:do {/file remove $_cdomain.cer; } on-error={ }\
\n:do {/file remove $_cdomain.key; } on-error={ }\
\ndelay 2;\
\n/ip service set www-ssl certificate=$_cdomain.cer_0;\
\n$ROUTER_OS_ADDITIONAL_SERVICES;\
\n\"
"
if ! _ssh_remote_cmd "$DEPLOY_SCRIPT_CMD"; then
return $_err_code
fi
if ! _ssh_remote_cmd "/system script run \"LECertDeploy-$_cdomain\""; then
return $_err_code
fi
if ! _ssh_remote_cmd "/system script remove \"LECertDeploy-$_cdomain\""; then
return $_err_code
fi
return 0
}
# inspired by deploy/ssh.sh
_ssh_remote_cmd() {
_cmd="$1"
_secure_debug "Remote commands to execute: $_cmd"
_info "Submitting sequence of commands to routeros"
# quotations in bash cmd below intended. Squash travis spellcheck error
# shellcheck disable=SC2029
$ROUTER_OS_SSH_CMD "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$_cmd"
_err_code="$?"
if [ "$_err_code" != "0" ]; then
_err "Error code $_err_code returned from routeros"
fi
return $_err_code
}
_scp_certificate() {
_src="$1"
_dst="$2"
_secure_debug "scp '$_src' to '$_dst'"
_info "Push key '$_src' to routeros"
$ROUTER_OS_SCP_CMD "$_src" "$_dst"
_err_code="$?"
if [ "$_err_code" != "0" ]; then
_err "Error code $_err_code returned from scp"
fi
return $_err_code
}
-200
View File
@@ -1,200 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to Ruckus ZoneDirector / Unleashed.
#
# Public domain, 2024, Tony Rielly <https://github.com/ms264556>
#
# ```sh
# acme.sh --deploy -d ruckus.example.com --deploy-hook ruckus
# ```
#
# Then you need to set the environment variables for the
# deploy script to work.
#
# ```sh
# export RUCKUS_HOST=myruckus.example.com
# export RUCKUS_USER=myruckususername
# export RUCKUS_PASS=myruckuspassword
#
# acme.sh --deploy -d myruckus.example.com --deploy-hook ruckus
# ```
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
ruckus_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_err_code=0
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf RUCKUS_HOST
_getdeployconf RUCKUS_USER
_getdeployconf RUCKUS_PASS
if [ -z "$RUCKUS_HOST" ]; then
_debug "Using _cdomain as RUCKUS_HOST, please set if not correct."
RUCKUS_HOST="$_cdomain"
fi
if [ -z "$RUCKUS_USER" ]; then
_err "Need to set the env variable RUCKUS_USER"
return 1
fi
if [ -z "$RUCKUS_PASS" ]; then
_err "Need to set the env variable RUCKUS_PASS"
return 1
fi
_savedeployconf RUCKUS_HOST "$RUCKUS_HOST"
_savedeployconf RUCKUS_USER "$RUCKUS_USER"
_savedeployconf RUCKUS_PASS "$RUCKUS_PASS"
_debug RUCKUS_HOST "$RUCKUS_HOST"
_debug RUCKUS_USER "$RUCKUS_USER"
_secure_debug RUCKUS_PASS "$RUCKUS_PASS"
export ACME_HTTP_NO_REDIRECTS=1
_info "Discovering the login URL"
_get "https://$RUCKUS_HOST" >/dev/null
_login_url="$(_response_header 'Location')"
if [ -n "$_login_url" ]; then
_login_path=$(echo "$_login_url" | sed 's|https\?://[^/]\+||')
if [ -z "$_login_path" ]; then
# redirect was to a different host
_err "Connection failed: redirected to a different host. Configure Unleashed with a Preferred Master or Management Interface."
return 1
fi
fi
if [ -z "${_login_url}" ]; then
_err "Connection failed: couldn't find login page."
return 1
fi
_base_url=$(dirname "$_login_url")
_login_page=$(basename "$_login_url")
if [ "$_login_page" = "index.html" ]; then
_err "Connection temporarily unavailable: Unleashed Rebuilding."
return 1
fi
if [ "$_login_page" = "wizard.jsp" ]; then
_err "Connection failed: Setup Wizard not complete."
return 1
fi
_info "Login"
_username_encoded="$(printf "%s" "$RUCKUS_USER" | _url_encode)"
_password_encoded="$(printf "%s" "$RUCKUS_PASS" | _url_encode)"
_login_query="$(printf "%s" "username=${_username_encoded}&password=${_password_encoded}&ok=Log+In")"
_post "$_login_query" "$_login_url" >/dev/null
_login_code="$(_response_code)"
if [ "$_login_code" = "200" ]; then
_err "Login failed: incorrect credentials."
return 1
fi
_info "Collect Session Cookie"
_H1="Cookie: $(_response_cookie)"
export _H1
_info "Collect CSRF Token"
_H2="X-CSRF-Token: $(_response_header 'HTTP_X_CSRF_TOKEN')"
export _H2
if _isRSA "$_ckey" >/dev/null 2>&1; then
_debug "Using RSA certificate."
else
_info "Verifying ECC certificate support."
_ul_version="$(_get_unleashed_version)"
if [ -z "$_ul_version" ]; then
_err "Your controller doesn't support ECC certificates. Please deploy an RSA certificate."
return 1
fi
_ul_version_major="$(echo "$_ul_version" | cut -d . -f 1)"
_ul_version_minor="$(echo "$_ul_version" | cut -d . -f 2)"
if [ "$_ul_version_major" -lt "200" ]; then
_err "ZoneDirector doesn't support ECC certificates. Please deploy an RSA certificate."
return 1
elif [ "$_ul_version_minor" -lt "13" ]; then
_err "Unleashed $_ul_version_major.$_ul_version_minor doesn't support ECC certificates. Please deploy an RSA certificate or upgrade to Unleashed 200.13+."
return 1
fi
_debug "ECC certificates OK for Unleashed $_ul_version_major.$_ul_version_minor."
fi
_info "Uploading certificate"
_post_upload "uploadcert" "$_cfullchain"
_info "Uploading private key"
_post_upload "uploadprivatekey" "$_ckey"
_info "Replacing certificate"
_replace_cert_ajax='<ajax-request action="docmd" comp="system" updater="rid.0.5" xcmd="replace-cert" checkAbility="6" timeout="-1"><xcmd cmd="replace-cert" cn="'$RUCKUS_HOST'"/></ajax-request>'
_post "$_replace_cert_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
_info "Rebooting"
_cert_reboot_ajax='<ajax-request action="docmd" comp="worker" updater="rid.0.5" xcmd="cert-reboot" checkAbility="6"><xcmd cmd="cert-reboot" action="undefined"/></ajax-request>'
_post "$_cert_reboot_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
return 0
}
_response_code() {
_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n" | _egrep_o "^[0-9]*"
}
_response_header() {
grep <"$HTTP_HEADER" -i "^$1:" | cut -d ':' -f 2- | tr -d "\r\n\t "
}
_response_cookie() {
_response_header 'Set-Cookie' | sed 's/;.*//'
}
_get_unleashed_version() {
_post '<ajax-request action="getstat" comp="system"><sysinfo/></ajax-request>' "$_base_url/_cmdstat.jsp" | _egrep_o "version-num=\"[^\"]*\"" | cut -d '"' -f 2
}
_post_upload() {
_post_action="$1"
_post_upfile="$2"
_post_boundary="----FormBoundary$(date "+%s%N")"
_post_data="$({
printf -- "--%s\r\n" "$_post_boundary"
printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
printf -- "Content-Type: application/octet-stream\r\n\r\n"
printf -- "%s\r\n" "$(cat "$_post_upfile")"
printf -- "--%s\r\n" "$_post_boundary"
printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
printf -- "%s\r\n" "$_post_action"
printf -- "--%s\r\n" "$_post_boundary"
printf -- "Content-Disposition: form-data; name=\"callback\"\r\n\r\n"
printf -- "%s\r\n" "uploader_$_post_action"
printf -- "--%s--\r\n\r\n" "$_post_boundary"
})"
_post "$_post_data" "$_base_url/_upload.jsp?request_type=xhr" "" "" "multipart/form-data; boundary=$_post_boundary" >/dev/null
}
-280
View File
@@ -1,280 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to a Shelly Gen3+ device.
# Deploy the HTTPS server certificate to a Shelly device on the local network.
#
# ```sh
# export SHELLY_HOST=192.168.1.100
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
# ```
#
# Environment variables:
# SHELLY_HOST (required) IP or hostname of the Shelly device
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
# Omit if auth is disabled on the device.
# SHELLY_USER (optional) Username for auth. Default: admin
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
# Default: 1 (reboot after upload)
#
# Requirements:
# - Shelly Gen3+ device (Gen4 recommended)
# - Firmware 2.0.0+ for HTTPS server certificate support
# - curl or wget
# - openssl (for SHA-256 digest and random cnonce)
#
# The device must be reachable via HTTP on the local network.
# The hook uploads the fullchain.pem and private key,
# then reboots the device to apply the new certificate.
#
# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since
# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
shelly_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf SHELLY_HOST
_getdeployconf SHELLY_PASSWORD
_getdeployconf SHELLY_USER
_getdeployconf SHELLY_REBOOT
_debug SHELLY_HOST "$SHELLY_HOST"
_debug SHELLY_USER "$SHELLY_USER"
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
if [ -z "$SHELLY_HOST" ]; then
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
return 1
fi
SHELLY_USER="${SHELLY_USER:-admin}"
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
_savedeployconf SHELLY_HOST "$SHELLY_HOST"
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
_savedeployconf SHELLY_USER "$SHELLY_USER"
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
# --- Auth handshake (only if password is set) ---
_shelly_auth_header=""
if [ -n "$SHELLY_PASSWORD" ]; then
_info "Authenticating to Shelly device at $SHELLY_HOST"
if ! _shelly_handshake; then
_err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility."
return 1
fi
_info "Authentication successful"
fi
# --- Upload certificate ---
_info "Uploading certificate to Shelly device at $SHELLY_HOST"
if ! _shelly_upload_cert; then
_err "Certificate upload failed"
return 1
fi
# --- Upload key ---
_info "Uploading private key to Shelly device"
if ! _shelly_upload_key; then
_err "Private key upload failed"
return 1
fi
_info "Certificate and key uploaded successfully"
# --- Reboot ---
if [ "$SHELLY_REBOOT" != "0" ]; then
_info "Rebooting Shelly device to apply certificate"
# Reboot may close the connection before sending a response
_shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)"
_info "Reboot command sent. Device will restart shortly."
else
_info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart."
fi
# Clear auth header so it does not leak to other hooks
export _H1=""
return 0
}
# --- Helper functions ---
# Perform RFC 7616 HTTP Digest auth handshake.
# Sets _shelly_auth_header on success (the Authorization header value).
_shelly_handshake() {
_inithttp
_debug "Probing device for auth challenge"
# Use a protected method (Shelly.GetStatus) to trigger 401.
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
_post '{"id":1,"method":"Shelly.GetStatus"}' \
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
# Detect auth from HTTP status line rather than response body
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
# No auth challenge — device accepted the request without credentials
_debug "Device responded without auth challenge. Proceeding without auth."
return 0
fi
_shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')"
_shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')"
_shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')"
if [ -z "$_shelly_nonce" ]; then
_err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?"
return 1
fi
_shelly_qop="${_shelly_qop:-auth}"
_debug "Shelly realm: $_shelly_realm"
_debug "Shelly qop: $_shelly_qop"
_secure_debug "Shelly nonce" "$_shelly_nonce"
# ha1 = SHA256(username:realm:password)
_shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)"
_secure_debug "Shelly ha1" "$_shelly_ha1"
# Generate client nonce (openssl is required for _digest, so always available)
_shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)"
_debug "Shelly cnonce: $_shelly_cnonce"
# Build the digest Authorization header value (stored for reuse)
_shelly_nc=1
_shelly_build_auth_header
return 0
}
# Check whether the HTTP response headers contain a digest auth challenge.
# Returns 0 (true) if a 401 with WWW-Authenticate is present.
_shelly_has_auth_challenge() {
_shelly_headers_file="$1"
_shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')"
[ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file"
}
# Build or rebuild the RFC 7616 Authorization header.
# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc
# Sets: _shelly_auth_header
_shelly_build_auth_header() {
_shelly_nc_hex="$(printf '%08x' "$_shelly_nc")"
# ha2 = SHA256(POST:/rpc)
_shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)"
# response = SHA256(ha1:nonce:nc:cnonce:qop:ha2)
_shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)"
# Build the Authorization header value (without the "Authorization: " prefix)
_shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256"
_secure_debug "Authorization header" "$_shelly_auth_header"
}
# Make a Shelly JSON-RPC call.
# Usage: _shelly_rpc <method> <params_json>
# Returns 0 on success, 1 on error.
_shelly_rpc() {
_shelly_method="$1"
_shelly_params="$2"
_shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}'
_debug "RPC method: $_shelly_method"
_debug2 "RPC body: $_shelly_body"
# shellcheck disable=SC2090
if [ -n "$_shelly_auth_header" ]; then
export _H1="Authorization: $_shelly_auth_header"
else
export _H1=""
fi
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
_shelly_ret=$?
if [ "$_shelly_ret" != "0" ]; then
_err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)"
return 1
fi
# Empty response means something went wrong (auth required but not provided, etc.)
if [ -z "$response" ]; then
_err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD."
return 1
fi
# Validate response looks like a Shelly JSON-RPC response.
# Catches non-JSON responses such as HTTP 429 "Too Many Requests" which
# would otherwise pass the empty and "error" checks below.
if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then
_err "Invalid response from Shelly device: $response"
return 1
fi
# Check for JSON-RPC error in response
if _contains "$response" '"error"'; then
_err "RPC error from Shelly: $response"
return 1
fi
_debug "RPC response: $response"
# Increment nonce counter and rebuild auth header for next request
if [ -n "$_shelly_auth_header" ]; then
_shelly_nc=$((_shelly_nc + 1))
_shelly_build_auth_header
fi
return 0
}
# Upload the certificate to the device.
# Note: We do NOT clear the existing certificate first, because the Shelly
# auto-removes all three files (cert, key, CA) when any one is cleared.
# Uploading overwrites in place — no clearing needed.
_shelly_upload_cert() {
_shelly_cert_data="$(_json_encode <"$_cfullchain")"
_debug "Uploading certificate"
if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then
_err "Failed to upload certificate to device"
return 1
fi
return 0
}
# Upload the private key to the device.
# Note: Do not clear first — see _shelly_upload_cert for rationale.
_shelly_upload_key() {
_shelly_key_data="$(_json_encode <"$_ckey")"
_debug "Uploading key"
if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then
_err "Failed to upload key to device"
return 1
fi
return 0
}
-491
View File
@@ -1,491 +0,0 @@
#!/usr/bin/env sh
# Script to deploy certificates to remote server by SSH
# Note that SSH must be able to login to remote host without a password...
# SSH Keys must have been exchanged with the remote host. Validate and
# test that you can login to USER@SERVER from the host running acme.sh before
# using this script.
#
# The following variables exported from environment will be used.
# If not set then values previously saved in domain.conf file are used.
#
# Only a username is required. All others are optional.
#
# The following examples are for QNAP NAS running QTS 4.2
# export DEPLOY_SSH_CMD="" # defaults to "ssh -T"
# export DEPLOY_SSH_USER="admin" # required
# export DEPLOY_SSH_SERVER="host1 host2:8022 192.168.0.1:9022" # defaults to domain name, support multiple servers with optional port
# export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem"
# export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem"
# export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem"
# export DEPLOY_SSH_FULLCHAIN=""
# export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart"
# export DEPLOY_SSH_BACKUP="" # yes or no, default to yes or previously saved value
# export DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy" # path on remote system. Defaults to .acme_ssh_deploy
# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value
# export DEPLOY_SSH_USE_SCP="" yes or no, default to no
# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q"
# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c
# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes
######## Public functions #####################
#domain keyfile certfile cafile fullchain
ssh_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_deploy_ssh_servers=""
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# USER is required to login by SSH to remote host.
_migratedeployconf Le_Deploy_ssh_user DEPLOY_SSH_USER
_getdeployconf DEPLOY_SSH_USER
_debug2 DEPLOY_SSH_USER "$DEPLOY_SSH_USER"
if [ -z "$DEPLOY_SSH_USER" ]; then
_err "DEPLOY_SSH_USER not defined."
return 1
fi
_savedeployconf DEPLOY_SSH_USER "$DEPLOY_SSH_USER"
# SERVER is optional. If not provided then use _cdomain
_migratedeployconf Le_Deploy_ssh_server DEPLOY_SSH_SERVER
_getdeployconf DEPLOY_SSH_SERVER
_debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER"
if [ -z "$DEPLOY_SSH_SERVER" ]; then
DEPLOY_SSH_SERVER="$_cdomain"
fi
_savedeployconf DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER"
# CMD is optional. If not provided then use ssh
_migratedeployconf Le_Deploy_ssh_cmd DEPLOY_SSH_CMD
_getdeployconf DEPLOY_SSH_CMD
_debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
if [ -z "$DEPLOY_SSH_CMD" ]; then
DEPLOY_SSH_CMD="ssh -T"
fi
_savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
# REMOTE_SHELL is optional. If not provided then use sh
_migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL
_getdeployconf DEPLOY_SSH_REMOTE_SHELL
_debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then
DEPLOY_SSH_REMOTE_SHELL="sh -c"
fi
_savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
# REMOTE_CMD_QUOTE is optional. If not provided then yes
_migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE
_getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE
_debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then
DEPLOY_SSH_REMOTE_CMD_QUOTE="yes"
fi
_savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
# BACKUP is optional. If not provided then default to previously saved value or yes.
_migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP
_getdeployconf DEPLOY_SSH_BACKUP
_debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP"
if [ -z "$DEPLOY_SSH_BACKUP" ]; then
DEPLOY_SSH_BACKUP="yes"
fi
_savedeployconf DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP"
# BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy
_migratedeployconf Le_Deploy_ssh_backup_path DEPLOY_SSH_BACKUP_PATH
_getdeployconf DEPLOY_SSH_BACKUP_PATH
_debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH"
if [ -z "$DEPLOY_SSH_BACKUP_PATH" ]; then
DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy"
fi
_savedeployconf DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH"
# MULTI_CALL is optional. If not provided then default to previously saved
# value (which may be undefined... equivalent to "no").
_migratedeployconf Le_Deploy_ssh_multi_call DEPLOY_SSH_MULTI_CALL
_getdeployconf DEPLOY_SSH_MULTI_CALL
_debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL"
if [ -z "$DEPLOY_SSH_MULTI_CALL" ]; then
DEPLOY_SSH_MULTI_CALL="no"
fi
_savedeployconf DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL"
# KEYFILE is optional.
# If provided then private key will be copied to provided filename.
_migratedeployconf Le_Deploy_ssh_keyfile DEPLOY_SSH_KEYFILE
_getdeployconf DEPLOY_SSH_KEYFILE
_debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE"
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
_savedeployconf DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE"
fi
# CERTFILE is optional.
# If provided then certificate will be copied or appended to provided filename.
_migratedeployconf Le_Deploy_ssh_certfile DEPLOY_SSH_CERTFILE
_getdeployconf DEPLOY_SSH_CERTFILE
_debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE"
if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
_savedeployconf DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE"
fi
# CAFILE is optional.
# If provided then CA intermediate certificate will be copied or appended to provided filename.
_migratedeployconf Le_Deploy_ssh_cafile DEPLOY_SSH_CAFILE
_getdeployconf DEPLOY_SSH_CAFILE
_debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE"
if [ -n "$DEPLOY_SSH_CAFILE" ]; then
_savedeployconf DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE"
fi
# FULLCHAIN is optional.
# If provided then fullchain certificate will be copied or appended to provided filename.
_migratedeployconf Le_Deploy_ssh_fullchain DEPLOY_SSH_FULLCHAIN
_getdeployconf DEPLOY_SSH_FULLCHAIN
_debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN"
if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then
_savedeployconf DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN"
fi
# REMOTE_CMD is optional.
# If provided then this command will be executed on remote host.
_migratedeployconf Le_Deploy_ssh_remote_cmd DEPLOY_SSH_REMOTE_CMD
_getdeployconf DEPLOY_SSH_REMOTE_CMD
_debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD"
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
_savedeployconf DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD"
fi
# USE_SCP is optional. If not provided then default to previously saved
# value (which may be undefined... equivalent to "no").
_getdeployconf DEPLOY_SSH_USE_SCP
_debug2 DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP"
if [ -z "$DEPLOY_SSH_USE_SCP" ]; then
DEPLOY_SSH_USE_SCP="no"
fi
_savedeployconf DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP"
# SCP_CMD is optional. If not provided then use scp
_getdeployconf DEPLOY_SSH_SCP_CMD
_debug2 DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD"
if [ -z "$DEPLOY_SSH_SCP_CMD" ]; then
DEPLOY_SSH_SCP_CMD="scp -q"
fi
_savedeployconf DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD"
if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then
DEPLOY_SSH_MULTI_CALL="yes"
_info "Using scp as alternate method for copying files. Multicall Mode is implicit"
elif [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
_info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host"
else
_info "Required commands batched and sent in single call to remote host"
fi
_returnCode=0
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
if ! _ssh_deploy; then
# in case of an error, remember it, but keep going for the remaining servers
_returnCode=1
fi
done
return $_returnCode
}
_ssh_deploy() {
_err_code=0
_cmdstr=""
_backupprefix=""
_backupdir=""
_local_cert_file=""
_local_ca_file=""
_local_full_file=""
case $DEPLOY_SSH_SERVER in
*:*)
_host=${DEPLOY_SSH_SERVER%:*}
_port=${DEPLOY_SSH_SERVER##*:}
;;
*)
_host=$DEPLOY_SSH_SERVER
_port=
;;
esac
_info "Deploy certificates to remote server $DEPLOY_SSH_USER@$_host:$_port"
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
_backupprefix="$DEPLOY_SSH_BACKUP_PATH/$_cdomain-backup"
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
# run cleanup on the backup directory, erase all older
# than 180 days (15552000 seconds).
_cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
_info "Backup of old certificate files will be placed in remote directory $_backupdir"
_info "Backup directories erased after 180 days."
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
# copy new key into file.
if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then
# scp the file
if ! _scp_remote_cmd "$_ckey" "$DEPLOY_SSH_KEYFILE"; then
return $_err_code
fi
else
# If file doesn't exist, create it and change its permissions.
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;"
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
fi
if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
_pipe=">"
if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then
# if filename is same as previous file then append.
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
# copy new certificate into file.
if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then
# scp the file
_local_cert_file=$(_mktemp)
if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then
cat "$_ckey" >>"$_local_cert_file"
fi
cat "$_ccert" >>"$_local_cert_file"
if ! _scp_remote_cmd "$_local_cert_file" "$DEPLOY_SSH_CERTFILE"; then
return $_err_code
fi
else
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;"
_info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
fi
if [ -n "$DEPLOY_SSH_CAFILE" ]; then
_pipe=">"
if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ] ||
[ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then
# if filename is same as previous file then append.
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
# copy new certificate into file.
if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then
# scp the file
_local_ca_file=$(_mktemp)
if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ]; then
cat "$_ckey" >>"$_local_ca_file"
fi
if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then
cat "$_ccert" >>"$_local_ca_file"
fi
cat "$_cca" >>"$_local_ca_file"
if ! _scp_remote_cmd "$_local_ca_file" "$DEPLOY_SSH_CAFILE"; then
return $_err_code
fi
else
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;"
_info "will copy CA file to remote file $DEPLOY_SSH_CAFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
fi
if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then
_pipe=">"
if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ] ||
[ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ] ||
[ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then
# if filename is same as previous file then append.
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
# copy new certificate into file.
if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then
# scp the file
_local_full_file=$(_mktemp)
if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ]; then
cat "$_ckey" >>"$_local_full_file"
fi
if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ]; then
cat "$_ccert" >>"$_local_full_file"
fi
if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then
cat "$_cca" >>"$_local_full_file"
fi
cat "$_cfullchain" >>"$_local_full_file"
if ! _scp_remote_cmd "$_local_full_file" "$DEPLOY_SSH_FULLCHAIN"; then
return $_err_code
fi
else
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;"
_info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
fi
# cleanup local files if any
if [ -f "$_local_cert_file" ]; then
rm -f "$_local_cert_file"
fi
if [ -f "$_local_ca_file" ]; then
rm -f "$_local_ca_file"
fi
if [ -f "$_local_full_file" ]; then
rm -f "$_local_full_file"
fi
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
_cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;"
_info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
_cmdstr=""
fi
fi
# if commands not all sent in multiple calls then all commands sent in a single SSH call now...
if [ -n "$_cmdstr" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
fi
# cleanup in case all is ok
return 0
}
#cmd
_ssh_remote_cmd() {
_cmd="$1"
_ssh_cmd="$DEPLOY_SSH_CMD"
if [ -n "$_port" ]; then
_ssh_cmd="$_ssh_cmd -p $_port"
fi
_secure_debug "Remote commands to execute: $_cmd"
_info "Submitting sequence of commands to remote server by $_ssh_cmd"
if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then
# quotations in bash cmd below intended. Squash travis spellcheck error
# shellcheck disable=SC2029
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'"
else
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd"
fi
_err_code="$?"
if [ "$_err_code" != "0" ]; then
_err "Error code $_err_code returned from ssh"
fi
return $_err_code
}
# cmd scp
_scp_remote_cmd() {
_src=$1
_dest=$2
_scp_cmd="$DEPLOY_SSH_SCP_CMD"
if [ -n "$_port" ]; then
_scp_cmd="$_scp_cmd -P $_port"
fi
_secure_debug "Remote copy source $_src to destination $_dest"
_info "Submitting secure copy by $_scp_cmd"
$_scp_cmd "$_src" "$DEPLOY_SSH_USER"@"$_host":"$_dest"
_err_code="$?"
if [ "$_err_code" != "0" ]; then
_err "Error code $_err_code returned from scp"
fi
return $_err_code
}
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env sh
#Here is a sample custom api script.
#This file name is "myapi.sh"
#So, here must be a method myapi_deploy()
#Which will be called by acme.sh to deploy the cert
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
strongswan_deploy() {
_cdomain="${1}"
_ckey="${2}"
_ccert="${3}"
_cca="${4}"
_cfullchain="${5}"
_info "Using strongswan"
if _exists ipsec; then
_ipsec=ipsec
elif _exists strongswan; then
_ipsec=strongswan
fi
if _exists swanctl; then
_swanctl=swanctl
fi
# For legacy stroke mode
if [ -n "${_ipsec}" ]; then
_info "${_ipsec} command detected"
_confdir=$(${_ipsec} --confdir)
if [ -z "${_confdir}" ]; then
_err "no strongswan --confdir is detected"
return 1
fi
_info _confdir "${_confdir}"
__deploy_cert "stroke" "${_confdir}" "$@"
${_ipsec} reload
fi
# For modern vici mode
if [ -n "${_swanctl}" ]; then
_info "${_swanctl} command detected"
for _dir in /usr/local/etc/swanctl /etc/swanctl /etc/strongswan/swanctl; do
if [ -d ${_dir} ]; then
_confdir=${_dir}
_info _confdir "${_confdir}"
break
fi
done
if [ -z "${_confdir}" ]; then
_err "no swanctl config dir is found"
return 1
fi
__deploy_cert "vici" "${_confdir}" "$@"
${_swanctl} --load-creds
fi
if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then
_err "no strongswan or ipsec command is detected"
_err "no swanctl is detected"
return 1
fi
}
#################### Private functions below ##################################
__deploy_cert() {
_swan_mode="${1}"
_confdir="${2}"
_cdomain="${3}"
_ckey="${4}"
_ccert="${5}"
_cca="${6}"
_cfullchain="${7}"
_debug _cdomain "${_cdomain}"
_debug _ckey "${_ckey}"
_debug _ccert "${_ccert}"
_debug _cca "${_cca}"
_debug _cfullchain "${_cfullchain}"
_debug _swan_mode "${_swan_mode}"
_debug _confdir "${_confdir}"
if [ "${_swan_mode}" = "vici" ]; then
_dir_private="private"
_dir_cert="x509"
_dir_ca="x509ca"
elif [ "${_swan_mode}" = "stroke" ]; then
_dir_private="ipsec.d/private"
_dir_cert="ipsec.d/certs"
_dir_ca="ipsec.d/cacerts"
else
_err "unknown StrongSwan mode ${_swan_mode}"
return 1
fi
cat "${_ckey}" >"${_confdir}/${_dir_private}/$(basename "${_ckey}")"
cat "${_ccert}" >"${_confdir}/${_dir_cert}/$(basename "${_ccert}")"
cat "${_cca}" >"${_confdir}/${_dir_ca}/$(basename "${_cca}")"
if [ "${_swan_mode}" = "stroke" ]; then
cat "${_cfullchain}" >"${_confdir}/${_dir_ca}/$(basename "${_cfullchain}")"
fi
}
-444
View File
@@ -1,444 +0,0 @@
#!/usr/bin/env sh
################################################################################
# ACME.sh 3rd party deploy plugin for Synology DSM
################################################################################
# Authors: Brian Hartvigsen (creator), https://github.com/tresni
# Martin Arndt (contributor), https://troublezone.net/
# Updated: 2023-07-03
# Issues: https://github.com/acmesh-official/acme.sh/issues/2727
################################################################################
# Usage (shown values are the examples):
# 1. Set required environment variables:
# - use automatically created temp admin user to authenticate
# export SYNO_USE_TEMP_ADMIN=1
# - or provide your own admin user credential to authenticate
# 1. export SYNO_USERNAME="adminUser"
# 2. export SYNO_PASSWORD="adminPassword"
# 2. Set optional environment variables
# - common optional variables
# - export SYNO_SCHEME="http" - defaults to "http"
# - export SYNO_HOSTNAME="localhost" - defaults to "localhost"
# - export SYNO_PORT="5000" - defaults to "5000"
# - export SYNO_CREATE=1 - to allow creating the cert if it doesn't exist
# - export SYNO_CERTIFICATE="" - to replace a specific cert by its
# description
# - temp admin optional variables
# - export SYNO_LOCAL_HOSTNAME=1 - if set to 1, force to treat hostname is
# targeting current local machine (since
# this method only locally supported)
# - exsiting admin 2FA-OTP optional variables
# - export SYNO_OTP_CODE="XXXXXX" - if set, script won't require to
# interactive input the OTP code
# - export SYNO_DEVICE_NAME="CertRenewal" - if set, script won't require to
# interactive input the device name
# - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead)
# required for omitting 2FA-OTP
# 3. Run command:
# acme.sh --deploy --deploy-hook synology_dsm -d example.com
################################################################################
# Dependencies:
# - curl
# - synouser & synogroup & synosetkeyvalue (Required for SYNO_USE_TEMP_ADMIN=1)
################################################################################
# Return value:
# 0 means success, otherwise error.
################################################################################
########## Public functions ####################################################
#domain keyfile certfile cafile fullchain
synology_dsm_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_debug _cdomain "$_cdomain"
# Get username and password, but don't save until we authenticated successfully
_migratedeployconf SYNO_Username SYNO_USERNAME
_migratedeployconf SYNO_Password SYNO_PASSWORD
_migratedeployconf SYNO_Device_ID SYNO_DEVICE_ID
_migratedeployconf SYNO_Device_Name SYNO_DEVICE_NAME
_getdeployconf SYNO_USERNAME
_getdeployconf SYNO_PASSWORD
_getdeployconf SYNO_DEVICE_ID
_getdeployconf SYNO_DEVICE_NAME
# Prepare to use temp admin if SYNO_USE_TEMP_ADMIN is set
_getdeployconf SYNO_USE_TEMP_ADMIN
_check2cleardeployconfexp SYNO_USE_TEMP_ADMIN
_debug2 SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
_err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
_err "Notice: temp admin user authorization method only supports local deployment on DSM."
return 1
fi
if synouser --help 2>&1 | grep -q 'Permission denied'; then
_err "For creating temp admin user, the deploy script must be run as root."
return 1
fi
[ -n "$SYNO_USERNAME" ] || _savedeployconf SYNO_USERNAME ""
[ -n "$SYNO_PASSWORD" ] || _savedeployconf SYNO_PASSWORD ""
_debug "Setting temp admin user credential..."
SYNO_USERNAME=sc-acmesh-tmp
SYNO_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
# Set 2FA-OTP settings to empty consider they won't be needed.
SYNO_DEVICE_ID=
SYNO_DEVICE_NAME=
SYNO_OTP_CODE=
else
_debug2 SYNO_USERNAME "$SYNO_USERNAME"
_secure_debug2 SYNO_PASSWORD "$SYNO_PASSWORD"
_debug2 SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
fi
if [ -z "$SYNO_USERNAME" ] || [ -z "$SYNO_PASSWORD" ]; then
_err "You must set either SYNO_USE_TEMP_ADMIN, or set both SYNO_USERNAME and SYNO_PASSWORD."
return 1
fi
# Optional scheme, hostname and port for Synology DSM
_migratedeployconf SYNO_Scheme SYNO_SCHEME
_migratedeployconf SYNO_Hostname SYNO_HOSTNAME
_migratedeployconf SYNO_Port SYNO_PORT
_getdeployconf SYNO_SCHEME
_getdeployconf SYNO_HOSTNAME
_getdeployconf SYNO_PORT
# Default values for scheme, hostname and port
# Defaulting to localhost and http, because it's localhost…
[ -n "$SYNO_SCHEME" ] || SYNO_SCHEME=http
[ -n "$SYNO_HOSTNAME" ] || SYNO_HOSTNAME=localhost
[ -n "$SYNO_PORT" ] || SYNO_PORT=5000
_savedeployconf SYNO_SCHEME "$SYNO_SCHEME"
_savedeployconf SYNO_HOSTNAME "$SYNO_HOSTNAME"
_savedeployconf SYNO_PORT "$SYNO_PORT"
_debug2 SYNO_SCHEME "$SYNO_SCHEME"
_debug2 SYNO_HOSTNAME "$SYNO_HOSTNAME"
_debug2 SYNO_PORT "$SYNO_PORT"
# Get the certificate description, but don't save it until we verify it's real
_migratedeployconf SYNO_Certificate SYNO_CERTIFICATE "base64"
_getdeployconf SYNO_CERTIFICATE
_check2cleardeployconfexp SYNO_CERTIFICATE
_debug SYNO_CERTIFICATE "${SYNO_CERTIFICATE:-}"
# shellcheck disable=SC1003 # We are not trying to escape a single quote
if printf "%s" "$SYNO_CERTIFICATE" | grep '\\'; then
_err "Do not use a backslash (\) in your certificate description"
return 1
fi
_debug "Getting API version..."
_base_url="$SYNO_SCHEME://$SYNO_HOSTNAME:$SYNO_PORT"
_debug _base_url "$_base_url"
response=$(_get "$_base_url/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth")
api_path=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p')
api_version=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"maxVersion" *: *\([0-9]*\).*/\1/p')
_debug3 response "$response"
_debug3 api_path "$api_path"
_debug3 api_version "$api_version"
# Login, get the session ID and SynoToken from JSON
_info "Logging into $SYNO_HOSTNAME:$SYNO_PORT..."
encoded_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)"
encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _url_encode)"
# ## START ## - DEPRECATED, for backward compatibility
_getdeployconf SYNO_TOTP_SECRET
if [ -n "$SYNO_TOTP_SECRET" ]; then
_info "WARNING: Usage of SYNO_TOTP_SECRET is deprecated!"
_info " See synology_dsm.sh script or ACME.sh Wiki page for details:"
_info " https://github.com/acmesh-official/acme.sh/wiki/Synology-NAS-Guide"
if ! _exists oathtool; then
_err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET"
return 1
fi
DEPRECATED_otp_code="$(oathtool --base32 --totp "$SYNO_TOTP_SECRET" 2>/dev/null)"
if [ -z "$SYNO_DEVICE_ID" ]; then
_getdeployconf SYNO_DID
[ -n "$SYNO_DID" ] || SYNO_DEVICE_ID="$SYNO_DID"
fi
if [ -n "$SYNO_DEVICE_ID" ]; then
_H1="Cookie: did=$SYNO_DEVICE_ID"
export _H1
_debug3 H1 "${_H1}"
fi
response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path?enable_syno_token=yes")
_debug3 response "$response"
# ## END ## - DEPRECATED, for backward compatibility
# If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP.
# Notice that if SYNO_USE_TEMP_ADMIN=1, both variables will be unset
else
if [ -n "$SYNO_DEVICE_ID" ] || [ -n "$SYNO_OTP_CODE" ]; then
response='{"error":{"code":403}}'
# Assume the current account disabled 2FA-OTP, try to log in right away.
else
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
_getdeployconf SYNO_LOCAL_HOSTNAME
_debug SYNO_LOCAL_HOSTNAME "${SYNO_LOCAL_HOSTNAME:-}"
if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
if [ "$SYNO_LOCAL_HOSTNAME" != "1" ]; then
_err "SYNO_USE_TEMP_ADMIN=1 only support local deployment, though if you are sure that the hostname $SYNO_HOSTNAME is targeting to your **current local machine**, execute 'export SYNO_LOCAL_HOSTNAME=1' then rerun."
return 1
fi
fi
_debug "Creating temp admin user in Synology DSM..."
if synogroup --help | grep -q '\-\-memberadd '; then
_temp_admin_create "$SYNO_USERNAME" "$SYNO_PASSWORD"
synogroup --memberadd administrators "$SYNO_USERNAME" >/dev/null
elif synogroup --help | grep -q '\-\-member '; then
# For supporting DSM 6.x which only has `--member` parameter.
cur_admins=$(synogroup --get administrators | awk -F '[][]' '/Group Members/,0{if(NF>1)printf "%s ", $2}')
if [ -n "$cur_admins" ]; then
_temp_admin_create "$SYNO_USERNAME" "$SYNO_PASSWORD"
_secure_debug3 admin_users "$cur_admins$SYNO_USERNAME"
# shellcheck disable=SC2086
synogroup --member administrators $cur_admins $SYNO_USERNAME >/dev/null
else
_err "The tool synogroup may be broken, please set SYNO_USERNAME and SYNO_PASSWORD instead."
return 1
fi
else
_err "Unsupported synogroup tool detected, please set SYNO_USERNAME and SYNO_PASSWORD instead."
return 1
fi
# havig a workaround to temporary disable enforce 2FA-OTP, will restore
# it soon (after a single request), though if any accident occurs like
# unexpected interruption, this setting can be easily reverted manually.
otp_enforce_option=$(synogetkeyvalue /etc/synoinfo.conf otp_enforce_option)
if [ -n "$otp_enforce_option" ] && [ "${otp_enforce_option:-"none"}" != "none" ]; then
synosetkeyvalue /etc/synoinfo.conf otp_enforce_option none
_info "Enforcing 2FA-OTP has been disabled to complete temp admin authentication."
_info "Notice: it will be restored soon, if not, you can restore it manually via Control Panel."
_info "previous_otp_enforce_option" "$otp_enforce_option"
else
otp_enforce_option=""
fi
fi
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes")
if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then
synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option"
_info "Restored previous enforce 2FA-OTP option."
fi
_debug3 response "$response"
fi
fi
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
# Account has 2FA-OTP enabled, since error 403 reported.
# https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
if [ "$error_code" = "403" ]; then
if [ -z "$SYNO_DEVICE_NAME" ]; then
printf "Enter device name or leave empty for default (CertRenewal): "
read -r SYNO_DEVICE_NAME
[ -n "$SYNO_DEVICE_NAME" ] || SYNO_DEVICE_NAME="CertRenewal"
fi
if [ -n "$SYNO_DEVICE_ID" ]; then
# Omit OTP code with SYNO_DEVICE_ID.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID")
_secure_debug3 response "$response"
else
# Require the OTP code if still unset.
if [ -z "$SYNO_OTP_CODE" ]; then
printf "Enter OTP code for user '%s': " "$SYNO_USERNAME"
read -r SYNO_OTP_CODE
fi
_secure_debug SYNO_OTP_CODE "${SYNO_OTP_CODE:-}"
if [ -z "$SYNO_OTP_CODE" ]; then
response='{"error":{"code":404}}'
else
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE")
_secure_debug3 response "$response"
id_property='device_id'
[ "${api_version}" -gt '6' ] || id_property='did'
SYNO_DEVICE_ID=$(echo "$response" | grep "$id_property" | sed -n 's/.*"'$id_property'" *: *"\([^"]*\).*/\1/p')
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
fi
fi
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
fi
if [ -n "$error_code" ]; then
if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
_cleardeployconf SYNO_DEVICE_ID
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
elif [ "$error_code" = "404" ]; then
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
elif [ "$error_code" = "406" ]; then
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
_err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
else
_err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
fi
elif [ "$error_code" = "400" ]; then
_err "Failed to authenticate, no such account or incorrect password."
elif [ "$error_code" = "401" ]; then
_err "Failed to authenticate with a non-existent account."
elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then
_err "Failed to authenticate, the account password has expired or must be changed."
else
_err "Failed to authenticate with error: $error_code."
fi
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p')
token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
_debug "Session ID" "$sid"
_debug SynoToken "$token"
if [ -z "$sid" ] || [ -z "$token" ]; then
# Still can't get necessary info even got no errors, may Synology have API updated?
_err "Unable to authenticate to $_base_url, you may report this by providing full log with '--debug 3'."
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
_H1="X-SYNO-TOKEN: $token"
export _H1
_debug2 H1 "${_H1}"
# Now that we know the username and password are good, save them if not in temp admin mode.
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
_cleardeployconf SYNO_USERNAME
_cleardeployconf SYNO_PASSWORD
_cleardeployconf SYNO_DEVICE_ID
_cleardeployconf SYNO_DEVICE_NAME
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
else
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
fi
_info "Getting certificates in Synology DSM..."
response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1&_sid=$sid" "$_base_url/webapi/entry.cgi")
_debug3 response "$response"
escaped_certificate="$(printf "%s" "$SYNO_CERTIFICATE" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g')"
_debug escaped_certificate "$escaped_certificate"
id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
_debug2 id "$id"
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
if [ -n "$error_code" ]; then
if [ "$error_code" -eq 105 ]; then
_err "Current user is not administrator and does not have sufficient permission for deploying."
else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
_migratedeployconf SYNO_Create SYNO_CREATE
_getdeployconf SYNO_CREATE
_debug2 SYNO_CREATE "$SYNO_CREATE"
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
# We've verified this certificate description is a thing, so save it
_savedeployconf SYNO_CERTIFICATE "$SYNO_CERTIFICATE" "base64"
_info "Generating form POST request..."
nl="\0015\0012"
delim="--------------------------$(_utc_date | tr -d -- '-: ')"
content="--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")\0012"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_ccert")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ccert")\0012"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"inter_cert\"; filename=\"$(basename "$_cca")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cca")\0012"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_CERTIFICATE}"
if echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then
_debug2 default "This is the default certificate"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}true"
else
_debug2 default "This is NOT the default certificate"
fi
content="$content${nl}--$delim--${nl}"
content="$(printf "%b_" "$content")"
content="${content%_}" # protect trailing \n
_info "Upload certificate to the Synology DSM."
response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}")
_debug3 response "$response"
if ! echo "$response" | grep '"error":' >/dev/null; then
if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
_info "Restart HTTP services succeeded."
else
_info "Restart HTTP services not necessary."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 0
else
_err "Unable to update certificate, got error response: $response."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
}
#################### Private functions below ##################################
_logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
# Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
# Note: this overwrites $response, so print any error message that needs it before calling.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response"
}
_temp_admin_create() {
_username="$1"
_password="$2"
synouser --del "$_username" >/dev/null 2>/dev/null
synouser --add "$_username" "$_password" "" 0 "" 0 >/dev/null
}
_temp_admin_cleanup() {
_flag=$1
_username=$2
if [ -n "${_flag}" ]; then
_debug "Cleanuping temp admin info..."
synouser --del "$_username" >/dev/null
fi
}
# key
_check2cleardeployconfexp() {
_key="$1"
_clear_key="CLEAR_$_key"
# Clear saved settings if explicitly requested
if [ -n "$(eval echo \$"$_clear_key")" ]; then
_debug2 "$_key: value cleared from config, exported value will be ignored."
_cleardeployconf "$_key"
eval "$_key"=
export "$_key"=
eval SAVED_"$_key"=
export SAVED_"$_key"=
fi
}
-272
View File
@@ -1,272 +0,0 @@
#!/usr/bin/env sh
# Here is a scipt to deploy the cert to your TrueNAS using the REST API.
# https://www.truenas.com/docs/hub/additional-topics/api/rest_api.html
#
# Written by Frank Plass github@f-plass.de
# https://github.com/danb35/deploy-freenas/blob/master/deploy_freenas.py
# Thanks to danb35 for your template!
#
# Following environment variables must be set:
#
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
#
# The following environmental variables may be set if you don't like their
# default values:
#
# DEPLOY_TRUENAS_HOSTNAME - defaults to localhost
# DEPLOY_TRUENAS_SCHEME - defaults to http, set alternatively to https
#
#returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
truenas_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_TRUENAS_APIKEY
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
return 1
fi
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
# Optional hostname, scheme for TrueNAS
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
_getdeployconf DEPLOY_TRUENAS_SCHEME
# default values for hostname and scheme
[ -n "${DEPLOY_TRUENAS_HOSTNAME}" ] || DEPLOY_TRUENAS_HOSTNAME="localhost"
[ -n "${DEPLOY_TRUENAS_SCHEME}" ] || DEPLOY_TRUENAS_SCHEME="http"
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME"
_api_url="$DEPLOY_TRUENAS_SCHEME://$DEPLOY_TRUENAS_HOSTNAME/api/v2.0"
_debug _api_url "$_api_url"
_H1="Authorization: Bearer $DEPLOY_TRUENAS_APIKEY"
_secure_debug3 _H1 "$_H1"
_info "Testing Connection TrueNAS"
_response=$(_get "$_api_url/system/state")
_info "TrueNAS system state: $_response."
_info "Getting TrueNAS version"
_response=$(_get "$_api_url/system/version")
if echo "$_response" | grep -q "SCALE"; then
_truenas_os=$(echo "$_response" | cut -d '-' -f 2)
_truenas_version=$(echo "$_response" | cut -d '-' -f 3 | tr -d '"' | cut -d '.' -f 1,2)
else
_truenas_os="unknown"
_truenas_version="unknown"
fi
_info "Detected TrueNAS system os: $_truenas_os"
_info "Detected TrueNAS system version: $_truenas_version"
if [ -z "$_response" ]; then
_err "Unable to authenticate to $_api_url."
_err 'Check your connection settings are correct, e.g.'
_err 'DEPLOY_TRUENAS_HOSTNAME="192.168.x.y" or DEPLOY_TRUENAS_HOSTNAME="truenas.example.com".'
_err 'DEPLOY_TRUENAS_SCHEME="https" or DEPLOY_TRUENAS_SCHEME="http".'
_err "Verify your TrueNAS API key is valid and set correctly, e.g. DEPLOY_TRUENAS_APIKEY=xxxx...."
return 1
fi
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME"
_info "Getting current active certificate from TrueNAS"
_response=$(_get "$_api_url/system/general")
_active_cert_id=$(echo "$_response" | grep -B2 '"name":' | grep 'id' | tr -d -- '"id: ,')
_active_cert_name=$(echo "$_response" | grep '"name":' | sed -n 's/.*: "\(.\{1,\}\)",$/\1/p')
_param_httpsredirect=$(echo "$_response" | grep '"ui_httpsredirect":' | sed -n 's/.*": \(.\{1,\}\),$/\1/p')
_debug Active_UI_Certificate_ID "$_active_cert_id"
_debug Active_UI_Certificate_Name "$_active_cert_name"
_debug Active_UI_http_redirect "$_param_httpsredirect"
if [ "$DEPLOY_TRUENAS_SCHEME" = "http" ] && [ "$_param_httpsredirect" = "true" ]; then
_info "HTTP->HTTPS redirection is enabled"
_info "Setting DEPLOY_TRUENAS_SCHEME to 'https'"
DEPLOY_TRUENAS_SCHEME="https"
_api_url="$DEPLOY_TRUENAS_SCHEME://$DEPLOY_TRUENAS_HOSTNAME/api/v2.0"
_savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME"
fi
_info "Uploading new certificate to TrueNAS"
_certname="Letsencrypt_$(_utc_date | tr ' ' '_' | tr -d -- ':')"
_debug3 _certname "$_certname"
_certData="{\"create_type\": \"CERTIFICATE_CREATE_IMPORTED\", \"name\": \"${_certname}\", \"certificate\": \"$(_json_encode <"$_cfullchain")\", \"privatekey\": \"$(_json_encode <"$_ckey")\"}"
_add_cert_result="$(_post "$_certData" "$_api_url/certificate" "" "POST" "application/json")"
_debug3 _add_cert_result "$_add_cert_result"
_info "Fetching list of installed certificates"
_cert_list=$(_get "$_api_url/system/general/ui_certificate_choices")
_cert_id=$(echo "$_cert_list" | grep "$_certname" | sed -n 's/.*"\([0-9]\{1,\}\)".*$/\1/p')
_debug3 _cert_id "$_cert_id"
_info "Current activate certificate ID: $_cert_id"
_activateData="{\"ui_certificate\": \"${_cert_id}\"}"
_activate_result="$(_post "$_activateData" "$_api_url/system/general" "" "PUT" "application/json")"
_debug3 _activate_result "$_activate_result"
_truenas_version_23_10="23.10"
_truenas_version_24_10="24.10"
_check_version=$(printf "%s\n%s" "$_truenas_version_23_10" "$_truenas_version" | sort -V | head -n 1)
if [ "$_truenas_os" != "SCALE" ] || [ "$_check_version" != "$_truenas_version_23_10" ]; then
_info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
_webdav_list=$(_get "$_api_url/webdav")
_webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
_info "Updating the WebDAV certificate"
_debug _webdav_cert_id "$_webdav_cert_id"
_webdav_data="{\"certssl\": \"${_cert_id}\"}"
_activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
_webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
_info "WebDAV certificate updated successfully"
else
_err "Unable to set WebDAV certificate"
_debug3 _activate_webdav_cert "$_activate_webdav_cert"
_debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
return 1
fi
_debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
else
_info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
fi
_info "Checking if S3 certificate is the same as the TrueNAS web UI"
_s3_list=$(_get "$_api_url/s3")
_s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
_info "Updating the S3 certificate"
_debug _s3_cert_id "$_s3_cert_id"
_s3_data="{\"certificate\": \"${_cert_id}\"}"
_activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
_s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
_info "S3 certificate updated successfully"
else
_err "Unable to set S3 certificate"
_debug3 _activate_s3_cert "$_activate_s3_cert"
_debug3 _s3_new_cert_id "$_s3_new_cert_id"
return 1
fi
_debug3 _activate_s3_cert "$_activate_s3_cert"
else
_info "S3 certificate is not configured or is not the same as TrueNAS web UI"
fi
fi
if [ "$_truenas_os" = "SCALE" ]; then
_check_version=$(printf "%s\n%s" "$_truenas_version_24_10" "$_truenas_version" | sort -V | head -n 1)
if [ "$_check_version" != "$_truenas_version_24_10" ]; then
_info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
if _exists jq; then
_info "Query all chart release"
_release_list=$(_get "$_api_url/chart/release")
_related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique")
_release_length=$(printf "%s" "$_related_name_list" | jq -r "length")
_info "Found $_release_length related chart release in list: $_related_name_list"
for i in $(seq 0 $((_release_length - 1))); do
_release_name=$(echo "$_related_name_list" | jq -r ".[$i]")
_info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name"
#Read the chart release configuration
_chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")")
#Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress
_updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert ) |= $_cert_id | .config.ingress ")
_update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")"
_debug3 _update_chart_result "$_update_chart_result"
done
else
_info "Tool 'jq' does not exists, skip chart release checking"
fi
else
_info "Checking if any app is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
if _exists jq; then
_info "Query all apps"
_app_list=$(_get "$_api_url/app")
_app_id_list=$(printf "%s" "$_app_list" | jq -r '.[].name')
_app_length=$(echo "$_app_id_list" | wc -l)
_info "Found $_app_length apps"
_info "Checking for each app if an update is needed"
for i in $(seq 1 "$_app_length"); do
_app_id=$(echo "$_app_id_list" | sed -n "${i}p")
_app_config="$(_post "\"$_app_id\"" "$_api_url/app/config" "" "POST" "application/json")"
# Check if the app use the same certificate TrueNAS web UI
_app_active_cert_config=$(echo "$_app_config" | tr -d '\000-\037' | _json_decode | jq -r ".ix_certificates[\"$_active_cert_id\"]")
if [ "$_app_active_cert_config" != "null" ]; then
_info "Updating certificate from $_active_cert_id to $_cert_id for app: $_app_id"
#Replace the old certificate id with the new one in path
_update_app_result="$(_post "{\"values\" : { \"network\": { \"certificate_id\": $_cert_id } } }" "$_api_url/app/id/$_app_id" "" "PUT" "application/json")"
_debug3 _update_app_result "$_update_app_result"
fi
done
else
_info "Tool 'jq' does not exists, skip app checking"
fi
fi
fi
_info "Checking if FTP certificate is the same as the TrueNAS web UI"
_ftp_list=$(_get "$_api_url/ftp")
_ftp_cert_id=$(echo "$_ftp_list" | grep '"ssltls_certificate":' | tr -d -- '"certislfa:_ ,')
if [ "$_ftp_cert_id" = "$_active_cert_id" ]; then
_info "Updating the FTP certificate"
_debug _ftp_cert_id "$_ftp_cert_id"
_ftp_data="{\"ssltls_certificate\": \"${_cert_id}\"}"
_activate_ftp_cert="$(_post "$_ftp_data" "$_api_url/ftp" "" "PUT" "application/json")"
_ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | grep '"ssltls_certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
if [ "$_ftp_new_cert_id" -eq "$_cert_id" ]; then
_info "FTP certificate updated successfully"
else
_err "Unable to set FTP certificate"
_debug3 _activate_ftp_cert "$_activate_ftp_cert"
_debug3 _ftp_new_cert_id "$_ftp_new_cert_id"
return 1
fi
_debug3 _activate_ftp_cert "$_activate_ftp_cert"
else
_info "FTP certificate is not configured or is not the same as TrueNAS web UI"
fi
_info "Deleting old certificate"
_delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")"
_debug3 _delete_result "$_delete_result"
_info "Reloading TrueNAS web UI"
_restart_UI=$(_get "$_api_url/system/general/ui_restart")
_debug2 _restart_UI "$_restart_UI"
if [ -n "$_add_cert_result" ] && [ -n "$_activate_result" ]; then
return 0
else
_err "Certificate update was not succesful, please try again with --debug"
return 1
fi
}
-518
View File
@@ -1,518 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2016
# TrueNAS deploy script for SCALE/CORE using websocket (websocat binary)
# It is recommend to use a wildcard certificate
#
# Tested with TrueNAS SCALE 25.10 (API "wss://host/api/current", JSON-RPC 2.0).
#
# Unlike "truenas_ws" hook, this script does NOT use midclt, the truenas_api_client Python package.
# It only depends on:
# - jq
# - websocat (a static binary you deploy)
#
# Why: avoids installing a Python environment / TrueNAS package on remote machine just to push a certificate.
#
# IMPORTANT: This script is written in pure POSIX sh (no coproc, no bash arrays).
#
#
# ---------------------------------------------------------------------------
# Environment variables
# ---------------------------------------------------------------------------
#
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
#
# Required:
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
#
# Optional:
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>" (required on first run)
# export DEPLOY_TRUENAS_PROTOCOL="ws" # ws or wss (default: ws)
# export DEPLOY_TRUENAS_PORT="80" # 80, 443, 8443 (default: 80)
# NOTE: defaults are intentionally "ws"/80, not "wss"/443: a freshly
# installed TrueNAS serves its Web UI over plain HTTP on port 80 out
# of the box, and port 443 is not listening until HTTPS is configured.
# Port 80 stays reachable even after HTTPS is enabled, so this keeps
# the hook working on first run without extra setup.
# export DEPLOY_TRUENAS_UPDATE_FTP="no" # yes or no (default: no) also updates the FTP certificate
# export DEPLOY_TRUENAS_UPDATE_APPS="no" # yes or no (default: no) also updates the certificate for any
# iX app exposing a "certificate_id" option.
# WARNING: this redeploys (restarts) every matching app.
# ---------------------------------------------------------------------------
########################
### Public functions ###
########################
# truenas_websocat_deploy
#
# Deploy new certificate to TrueNAS services with websocat binary
#
# Arguments
# 1: Domain
# 2: Key-File
# 3: Certificate-File
# 4: CA-File
# 5: FullChain-File
# Returns:
# 0: Success
# 1: Missing or invalid API Key
# 2: TrueNAS not ready (health check failed)
# 3: (reserved)
# 4: FTP & iX App cert error
# 5: WebUI cert error
# 6: Certificate creation job error
# 7: Websocat / transport call error (socket write/read failed)
# 8: Missing binary or invalid configuration
# 9: TrueNAS API returned an explicit error (JSON-RPC .error field)
truenas_websocat_deploy() {
_jq_bin=$(command -v jq 2>/dev/null)
if [ -z "$_jq_bin" ]; then
_err "jq binary not found in PATH. Install it using your system's package manager."
return 8
fi
_websocat_bin=$(command -v websocat 2>/dev/null)
if [ -z "$_websocat_bin" ]; then
_err "websocat binary not found in PATH. Install it using your system's package manager, or download a static binary from https://github.com/vi/websocat/releases."
return 8
fi
_domain="$1"
_file_key="$2"
_file_cert="$3"
_file_cca="$4"
_file_fullchain="$5"
_debug _domain "$_domain"
_debug _file_key "$_file_key"
_debug _file_cert "$_file_cert"
_debug _file_ca "$_file_cca"
_debug _file_fullchain "$_file_fullchain"
if [ ! -x "$_jq_bin" ]; then
_err "Binary not found or not executable: $_jq_bin"
return 8
fi
if [ ! -x "$_websocat_bin" ]; then
_err "Binary not found or not executable: $_websocat_bin"
return 8
fi
### ---- Configuration ----
_info "Checking environment variables..."
_getdeployconf DEPLOY_TRUENAS_APIKEY
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
_getdeployconf DEPLOY_TRUENAS_PORT
_getdeployconf DEPLOY_TRUENAS_UPDATE_FTP
_getdeployconf DEPLOY_TRUENAS_UPDATE_APPS
# Check API Key
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
return 1
fi
# Check Hostname, default to localhost if not set
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then
_info "TrueNAS hostname not set. Using 'localhost'."
DEPLOY_TRUENAS_HOSTNAME="localhost"
fi
# Check protocol, default to ws if not set: a freshly installed TrueNAS serves its Web UI over plain HTTP, so wss/443 is not available out of the box.
# Use DEPLOY_TRUENAS_PROTOCOL="wss" once HTTPS is configured, since the payload otherwise carries the API key and private key in plain text.
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then
_info "TrueNAS protocol not set. Using 'ws'."
DEPLOY_TRUENAS_PROTOCOL="ws"
fi
# Check port, default to 80 if not set (see protocol comment above)
if [ -z "$DEPLOY_TRUENAS_PORT" ]; then
_info "TrueNAS port not set. Using '80'."
DEPLOY_TRUENAS_PORT="80"
fi
case "$DEPLOY_TRUENAS_PORT" in
'' | *[!0-9]*)
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
return 8
;;
esac
_truenas_websocat_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/api/current"
# Check FTP update, default to no if not set
if [ -z "$DEPLOY_TRUENAS_UPDATE_FTP" ]; then
_info "Certificate update for FTP is not set. Using 'no'."
DEPLOY_TRUENAS_UPDATE_FTP="no"
fi
# Check Apps update, default to no if not set
if [ -z "$DEPLOY_TRUENAS_UPDATE_APPS" ]; then
_info "Certificate update for Apps is not set. Using 'no'."
DEPLOY_TRUENAS_UPDATE_APPS="no"
fi
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_debug2 DEPLOY_TRUENAS_UPDATE_FTP "$DEPLOY_TRUENAS_UPDATE_FTP"
_debug2 DEPLOY_TRUENAS_UPDATE_APPS "$DEPLOY_TRUENAS_UPDATE_APPS"
_debug _truenas_websocat_uri "$_truenas_websocat_uri"
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_info "Environment variables: OK"
### ---- Persistent WebSocket connection (FIFOs, sh/dash compatible) ----
#
# Authentication is tied to the WebSocket connection:
# the SAME connection must stay open from login until the end, otherwise every subsequent call comes back unauthenticated.
# We use two FIFOs + `exec` to talk to a background websocat process, without relying on bash-only extensions.
_websocat_tmpdir=$(mktemp -d /tmp/truenas_websocat.XXXXXX) || {
_err "mktemp failed"
return 3
}
_websocat_fifo_in="${_websocat_tmpdir}/in"
_websocat_fifo_out="${_websocat_tmpdir}/out"
mkfifo "$_websocat_fifo_in" "$_websocat_fifo_out" || {
_err "mkfifo failed"
rm -rf "$_websocat_tmpdir"
return 3
}
"$_websocat_bin" -n -k "$_truenas_websocat_uri" <"$_websocat_fifo_in" >"$_websocat_fifo_out" 2>"${_websocat_tmpdir}/err.log" &
_websocat_pid=$!
# Opening "in" for read+write avoids a deadlock if websocat hasn't opened the fifo for reading yet at the time we write to it.
exec 3<>"$_websocat_fifo_in"
exec 4<"$_websocat_fifo_out"
sleep 1
if ! kill -0 "$_websocat_pid" 2>/dev/null; then
_err "websocat exited prematurely."
_err "$(cat "${_websocat_tmpdir}/err.log" 2>/dev/null)"
exec 3>&- 4<&-
rm -rf "$_websocat_tmpdir"
return 3
fi
_websocat_req_counter=0
_truenas_websocat_cleanup() {
exec 3>&- 2>/dev/null
exec 4<&- 2>/dev/null
[ -n "$_websocat_pid" ] && kill "$_websocat_pid" 2>/dev/null
rm -rf "$_websocat_tmpdir" 2>/dev/null
}
# _truenas_websocat_rpc_call <method> <json_params>
# Does NOT log the payload/response: some calls (certificate.create, core.get_jobs) contain the certificate and private key in plain text, which would massively bloat the logs.
_truenas_websocat_rpc_call() {
_truenas_websocat_method="$1"
_truenas_websocat_params="$2"
_websocat_req_counter=$((_websocat_req_counter + 1))
_req_id="$_websocat_req_counter"
_truenas_websocat_payload=$("$_jq_bin" -c -n \
--arg jsonrpc "2.0" \
--arg id "$_req_id" \
--arg method "$_truenas_websocat_method" \
--argjson params "$_truenas_websocat_params" \
'{jsonrpc: $jsonrpc, id: $id, method: $method, params: $params}')
printf '%s\n' "$_truenas_websocat_payload" >&3 || {
_err "Socket write failed (method: $_truenas_websocat_method)"
return 7
}
IFS= read -r _truenas_websocat_response <&4 || {
_err "Socket read failed (method: $_truenas_websocat_method)"
return 7
}
printf '%s' "$_truenas_websocat_response"
}
# _truenas_websocat_rpc_has_error <response> <label> -> returns 0 (and prints) if an error was found, 1 otherwise
_truenas_websocat_rpc_has_error() {
_msg=$(printf '%s' "$1" | "$_jq_bin" -r '.error.message // empty' 2>/dev/null)
if [ -n "$_msg" ]; then
_err "RPC error ($2): $_msg"
return 0
fi
return 1
}
# Polls once per second and gives up after _TRUENAS_WEBSOCAT_JOB_TIMEOUT seconds (default 60s)
# if the job never leaves RUNNING/WAITING, so a stuck TrueNAS job cannot hang the deploy hook forever.
# NOTE: this same timeout also bounds app.update jobs when DEPLOY_TRUENAS_UPDATE_APPS=yes (iX App redeploy)
# raise _TRUENAS_WEBSOCAT_JOB_TIMEOUT if an app takes longer than that to redeploy (e.g. image pull, migrations).
_truenas_websocat_wait_for_job() {
_jobid="$1"
_truenas_websocat_job_elapsed=0
_truenas_websocat_job_timeout="${_TRUENAS_WEBSOCAT_JOB_TIMEOUT:-60}"
while true; do
if [ "$_truenas_websocat_job_elapsed" -ge "$_truenas_websocat_job_timeout" ]; then
_err "Job $_jobid: timed out after ${_truenas_websocat_job_timeout}s."
return 6
fi
sleep 1
_truenas_websocat_job_elapsed=$((_truenas_websocat_job_elapsed + 1))
_job_resp=$(_truenas_websocat_rpc_call "core.get_jobs" "[[[\"id\",\"=\",${_jobid}]]]") || return 6
if _truenas_websocat_rpc_has_error "$_job_resp" "core.get_jobs"; then return 6; fi
_state=$(printf '%s' "$_job_resp" | "$_jq_bin" -r '.result[0].state // empty')
case "$_state" in
SUCCESS)
printf '%s' "$_job_resp" | "$_jq_bin" -c '.result[0].result'
return 0
;;
FAILED | ABORTED)
_err "Job $_jobid failed: $(printf '%s' "$_job_resp" | "$_jq_bin" -c '.result[0].error')"
return 6
;;
"")
_err "Job $_jobid: unexpected response."
return 6
;;
esac
done
}
### ---- 1. Health check ----
_info "Testing connection to TrueNAS WebSocket at $_truenas_websocat_uri..."
_ping_resp=$(_truenas_websocat_rpc_call "core.ping" "[]") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_ping_resp" "core.ping"; then
_truenas_websocat_cleanup
return 9
fi
if [ "$(printf '%s' "$_ping_resp" | "$_jq_bin" -r '.result // empty')" != "pong" ]; then
_err "Health check failed (no pong received)."
_truenas_websocat_cleanup
return 2
fi
_info "Health check OK (pong received)."
### ---- 2. Authentication & Check ----
_info "Authenticating with API Key..."
_key_params=$("$_jq_bin" -c -n --arg k "$DEPLOY_TRUENAS_APIKEY" '[$k]')
_auth_resp=$(_truenas_websocat_rpc_call "auth.login_with_api_key" "$_key_params") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_auth_resp" "auth.login_with_api_key"; then
_truenas_websocat_cleanup
return 9
fi
if [ "$(printf '%s' "$_auth_resp" | "$_jq_bin" -r '.result // empty')" != "true" ]; then
_err "Authentication failed (invalid API key?)."
_truenas_websocat_cleanup
return 1
fi
_info "Connected to TrueNAS ($_truenas_websocat_uri)."
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
_savedeployconf DEPLOY_TRUENAS_UPDATE_FTP "$DEPLOY_TRUENAS_UPDATE_FTP"
_savedeployconf DEPLOY_TRUENAS_UPDATE_APPS "$DEPLOY_TRUENAS_UPDATE_APPS"
_info "Checking TrueNAS system version..."
_ver_resp=$(_truenas_websocat_rpc_call "system.info" "[]") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_ver_resp" "system.info"; then
_truenas_websocat_cleanup
return 9
fi
_sys_version=$(printf '%s' "$_ver_resp" | "$_jq_bin" -r '.result.version // .result // "Unknown"')
_info "TrueNAS System Version: $_sys_version"
### ---- 3. Read certificate files ----
_info "Reading certificate files for $_domain..."
if [ ! -f "$_file_fullchain" ] || [ ! -f "$_file_key" ]; then
_err "Certificate or key file not found."
_truenas_websocat_cleanup
return 5
fi
_cert_content=$("$_jq_bin" -sR . "$_file_fullchain")
_key_content=$("$_jq_bin" -sR . "$_file_key")
_safe_domain=$(echo "$_domain" | tr '*.' '_')
_cert_name="acme_${_safe_domain}_$(date +%Y%m%d_%H%M%S)"
_debug _certname "$_cert_name"
### ---- 4. Current Web UI certificate ----
_info "Retrieving current Web UI configuration..."
_config_resp=$(_truenas_websocat_rpc_call "system.general.config" "[]") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_config_resp" "system.general.config"; then
_truenas_websocat_cleanup
return 9
fi
_old_cert_id=$(printf '%s' "$_config_resp" | "$_jq_bin" -r '.result.ui_certificate.id // .result.ui_certificate // empty')
_info "Current Web UI Certificate ID: ${_old_cert_id:-None}"
### ---- 5. Import the new certificate (asynchronous job) ----
_info "Importing new certificate '$_cert_name'..."
_create_params=$("$_jq_bin" -n \
--arg name "$_cert_name" \
--argjson cert "$_cert_content" \
--argjson key "$_key_content" \
'[{create_type: "CERTIFICATE_CREATE_IMPORTED", name: $name, certificate: $cert, privatekey: $key}]')
_new_cert_resp=$(_truenas_websocat_rpc_call "certificate.create" "$_create_params") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_new_cert_resp" "certificate.create"; then
_truenas_websocat_cleanup
return 9
fi
_new_cert_jobid=$(printf '%s' "$_new_cert_resp" | "$_jq_bin" -r '.result // empty')
case "$_new_cert_jobid" in
'' | *[!0-9]*)
_err "Unexpected response from certificate.create (expected a job ID)."
_truenas_websocat_cleanup
return 6
;;
esac
_info "Import job certificate started (job ID: $_new_cert_jobid), waiting..."
_job_result=$(_truenas_websocat_wait_for_job "$_new_cert_jobid") || {
_truenas_websocat_cleanup
return 6
}
_new_cert_id=$(printf '%s' "$_job_result" | "$_jq_bin" -r '.id // empty')
if [ -z "$_new_cert_id" ]; then
_err "Could not retrieve the imported certificate's ID."
_truenas_websocat_cleanup
return 6
fi
_info "Certificate imported: '$_cert_name' (ID $_new_cert_id)."
### ---- 6. Assign to the Web UI ----
_info "Assigning certificate ID $_new_cert_id to Web UI..."
_update_resp=$(_truenas_websocat_rpc_call "system.general.update" "[{\"ui_certificate\": ${_new_cert_id}}]") || {
_truenas_websocat_cleanup
return 7
}
if _truenas_websocat_rpc_has_error "$_update_resp" "system.general.update"; then
_truenas_websocat_cleanup
return 9
fi
_assigned_id=$(printf '%s' "$_update_resp" | "$_jq_bin" -r '.result.ui_certificate.id // .result.ui_certificate // empty')
if [ "$_assigned_id" != "$_new_cert_id" ]; then
_err "Failed to assign the certificate to the Web UI."
_truenas_websocat_cleanup
return 5
fi
_info "Restarting TrueNAS Web UI service..."
_restart_resp=$(_truenas_websocat_rpc_call "system.general.ui_restart" "[]")
_truenas_websocat_rpc_has_error "$_restart_resp" "system.general.ui_restart"
_info "Web UI certificate updated and UI restarted."
# Need TrueNas to sleep before perform other actions
_info "Waiting for Web UI restart."
sleep 5
### ---- 7. FTP (optional) ----
if [ "$DEPLOY_TRUENAS_UPDATE_FTP" = "yes" ]; then
_info "Sending certicate for FTP service..."
_ftp_resp=$(_truenas_websocat_rpc_call "ftp.update" "[{\"ssltls_certificate\": ${_new_cert_id}}]") || {
_truenas_websocat_cleanup
return 4
}
if _truenas_websocat_rpc_has_error "$_ftp_resp" "ftp.update"; then
_err "Failed to update the FTP certificate."
_truenas_websocat_cleanup
return 4
else
_ftp_certid=$(printf '%s' "$_ftp_resp" | "$_jq_bin" -r '.result.ssltls_certificate // empty')
if [ "$_ftp_certid" = "$_new_cert_id" ]; then
_info "FTP certificate updated."
else
_err "FTP certificate: unexpected response."
_truenas_websocat_cleanup
return 4
fi
fi
fi
### ---- 8. iX Apps (optional - redeploys every matching app) ----
if [ "$DEPLOY_TRUENAS_UPDATE_APPS" = "yes" ]; then
_info "Sending certicate for iX Apps..."
_apps_resp=$(_truenas_websocat_rpc_call "app.query" "[]") || {
_truenas_websocat_cleanup
return 4
}
if _truenas_websocat_rpc_has_error "$_apps_resp" "app.query"; then
_err "Could not list apps."
_truenas_websocat_cleanup
return 4
else
for _app_name in $(printf '%s' "$_apps_resp" | "$_jq_bin" -r '.result[].name'); do
_app_cfg=$(_truenas_websocat_rpc_call "app.config" "[\"${_app_name}\"]") || {
_truenas_websocat_cleanup
return 4
}
_has_cert_opt=$(printf '%s' "$_app_cfg" | "$_jq_bin" -r '.result.network // {} | has("certificate_id")' 2>/dev/null)
if [ "$_has_cert_opt" = "true" ]; then
_info "Updating certificate for app '$_app_name' (this will redeploy it)..."
_app_update_resp=$(_truenas_websocat_rpc_call "app.update" "[\"${_app_name}\", {\"values\": {\"network\": {\"certificate_id\": ${_new_cert_id}}}}]") || {
_truenas_websocat_cleanup
return 4
}
_app_jobid=$(printf '%s' "$_app_update_resp" | "$_jq_bin" -r '.result // empty')
case "$_app_jobid" in
'' | *[!0-9]*)
_err "App '$_app_name': no job ID returned."
_truenas_websocat_cleanup
return 4
;;
*)
if ! _truenas_websocat_wait_for_job "$_app_jobid" >/dev/null; then
_err "App '$_app_name': update not confirmed."
_truenas_websocat_cleanup
return 4
fi
;;
esac
fi
done
fi
fi
### ---- 9. Delete the old certificate (non blocking) ----
if [ -n "$_old_cert_id" ] && [ "$_old_cert_id" != "$_new_cert_id" ] && [ "$_old_cert_id" != "null" ]; then
_del_resp=$(_truenas_websocat_rpc_call "certificate.delete" "[${_old_cert_id}]")
if ! _truenas_websocat_rpc_has_error "$_del_resp" "certificate.delete"; then
_del_jobid=$(printf '%s' "$_del_resp" | "$_jq_bin" -r '.result // empty')
case "$_del_jobid" in
'' | *[!0-9]*) : ;;
*)
_truenas_websocat_wait_for_job "$_del_jobid" >/dev/null || _info "Old certificate: deletion not confirmed ."
;;
esac
fi
fi
_truenas_websocat_cleanup
_info "TrueNAS deployment completed successfully."
return 0
}
-363
View File
@@ -1,363 +0,0 @@
#!/usr/bin/env sh
# TrueNAS deploy script for SCALE/CORE using websocket
# It is recommend to use a wildcard certificate
#
# Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
#
# Tested with TrueNAS Scale - Electric Eel 24.10
# Changes certificate in the following services:
# - Web UI
# - FTP
# - iX Apps
#
# The following environment variables must be set:
# ------------------------------------------------
#
# # API KEY
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
# Optional:
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>"
# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss
# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443
#
### Private functions
# Call websocket method
# Usage:
# _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
# _info "$_ws_response"
#
# Output:
# {"z": 9}
#
# Arguments:
# $@ - midclt arguments for call
#
# Returns:
# JSON/JOBID
_ws_call() {
_debug "_ws_call arg1" "$1"
_debug "_ws_call arg2" "$2"
_debug "_ws_call arg3" "$3"
# TrueNAS 26.0.0-BETA3 and later need a --plain option for midclt call, detect if it is available
_midclt_plain=""
case "$(midclt --help 2>/dev/null)" in
*--plain*) _midclt_plain="--plain" ;;
esac
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" $_midclt_plain call "$@")
_debug "_ws_response" "$_ws_response"
printf "%s" "$_ws_response"
return 0
}
# Upload certificate with webclient api
_ws_upload_cert() {
/usr/bin/env python - <<EOF
import sys
from truenas_api_client import Client
with Client(uri="$_ws_uri") as c:
### Login with API key
print("I:Trying to upload new certificate...")
ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
if ret:
### upload certificate
with open('$1', 'r') as file:
fullchain = file.read()
with open('$2', 'r') as file:
privatekey = file.read()
ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True)
print("R:" + str(ret["id"]))
sys.exit(0)
else:
print("R:0")
print("E:_ws_upload_cert error!")
sys.exit(7)
EOF
return $?
}
# Check argument is a number
# Usage:
#
# Output:
# n/a
#
# Arguments:
# $1 - Anything
#
# Returns:
# 0: true
# 1: false
_ws_check_jobid() {
case "$1" in
[0-9]*)
return 0
;;
esac
return 1
}
# Wait for job to finish and return result as JSON
# Usage:
# _ws_result=$(_ws_get_job_result "$_ws_jobid")
# _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
#
# Output:
# JSON result of the job
#
# Arguments:
# $1 - JobID
#
# Returns:
# n/a
_ws_get_job_result() {
while true; do
_sleep 2
_ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
_ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
_debug "_ws_result" "$_ws_result"
printf "%s" "$_ws_result"
_ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
if [ "$_ws_error" != "null" ]; then
_err "Job $1 failed:"
_err "$_ws_error"
return 7
fi
break
fi
done
return 0
}
########################
### Public functions ###
########################
# truenas_ws_deploy
#
# Deploy new certificate to TrueNAS services
#
# Arguments
# 1: Domain
# 2: Key-File
# 3: Certificate-File
# 4: CA-File
# 5: FullChain-File
# Returns:
# 0: Success
# 1: Missing API Key
# 2: TrueNAS not ready
# 3: Not a JobID
# 4: FTP cert error
# 5: WebUI cert error
# 6: Job error
# 7: WS call error
#
truenas_ws_deploy() {
_domain="$1"
_file_key="$2"
_file_cert="$3"
_file_ca="$4"
_file_fullchain="$5"
_debug _domain "$_domain"
_debug _file_key "$_file_key"
_debug _file_cert "$_file_cert"
_debug _file_ca "$_file_ca"
_debug _file_fullchain "$_file_fullchain"
########## Environment check
_info "Checking environment variables..."
_getdeployconf DEPLOY_TRUENAS_APIKEY
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
_getdeployconf DEPLOY_TRUENAS_PORT
# Check API Key
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
return 1
fi
# Check Hostname, default to localhost if not set
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then
_info "TrueNAS hostname not set. Using 'localhost'."
DEPLOY_TRUENAS_HOSTNAME="localhost"
fi
# Check protocol, default to ws if not set
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then
_info "TrueNAS protocol not set. Using 'ws'."
DEPLOY_TRUENAS_PROTOCOL="ws"
fi
# Check port, optional
if [ -n "$DEPLOY_TRUENAS_PORT" ]; then
case "$DEPLOY_TRUENAS_PORT" in
'' | *[!0-9]*)
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
return 8
;;
esac
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/websocket"
else
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
fi
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_debug _ws_uri "$_ws_uri"
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_info "Environment variables: OK"
########## Health check
_info "Checking TrueNAS health..."
_ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
_err "Error calling system.ready:"
_err "$_ws_response"
return $_ws_ret
fi
if [ "$_ws_response" != "TRUE" ]; then
_err "TrueNAS is not ready."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
_err "Verify API key."
return 2
fi
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
_info "TrueNAS health: OK"
########## System info
_info "Gather system info..."
_ws_response=$(_ws_call "system.info")
_truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
_info "TrueNAS version: $_truenas_version"
########## Gather current certificate
_info "Gather current WebUI certificate..."
_ws_response="$(_ws_call "system.general.config")"
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '.ui_certificate | if type == "object" then .id else . end')
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r 'if (.ui_certificate | type) == "object" then .ui_certificate.name else .ui_certificate_name end')
_info "Current WebUI certificate ID: $_ui_certificate_id"
_info "Current WebUI certificate name: $_ui_certificate_name"
########## Upload new certificate
_info "Upload new certificate..."
_certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
_info "New WebUI certificate name: $_certname"
_debug _certname "$_certname"
_ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
echo "$_ws_out" | while IFS= read -r LINE; do
case "$LINE" in
I:*)
_info "${LINE#I:}"
;;
D:*)
_debug "${LINE#D:}"
;;
E*)
_err "${LINE#E:}"
;;
*) ;;
esac
done
_new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
_info "New certificate ID: $_new_certid"
########## FTP
_info "Replace FTP certificate..."
_ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
_ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
if [ "$_ftp_certid" != "$_new_certid" ]; then
_err "Cannot set FTP certificate."
_debug "_ws_response" "$_ws_response"
return 4
fi
########## ix Apps (SCALE only)
_info "Replace app certificates..."
_ws_response=$(_ws_call "app.query")
for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
_info "Checking app $_app_name..."
_ws_response=$(_ws_call "app.config" "$_app_name")
if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
_info "App has certificate option, setup new certificate..."
_info "App will be redeployed after updating the certificate."
_ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
_debug "_ws_jobid" "$_ws_jobid"
if ! _ws_check_jobid "$_ws_jobid"; then
_err "No JobID returned from websocket method."
return 3
fi
_ws_result=$(_ws_get_job_result "$_ws_jobid")
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
return $_ws_ret
fi
_debug "_ws_result" "$_ws_result"
_info "App certificate replaced."
else
_info "App has no certificate option, skipping..."
fi
done
########## WebUI
_info "Replace WebUI certificate..."
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '.ui_certificate | if type == "object" then .id else . end')
if [ "$_changed_certid" != "$_new_certid" ]; then
_err "WebUI certificate change error.."
return 5
else
_info "WebUI certificate replaced."
fi
_info "Restarting WebUI..."
_ws_response=$(_ws_call "system.general.ui_restart")
_info "Waiting for UI restart..."
_sleep 15
########## Certificates
_info "Deleting old certificate..."
_ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
if ! _ws_check_jobid "$_ws_jobid"; then
_err "No JobID returned from websocket method."
return 3
fi
_ws_result=$(_ws_get_job_result "$_ws_jobid")
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
return $_ws_ret
fi
_info "Have a nice day...bye!"
}
-307
View File
@@ -1,307 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert on a Unifi Controller or Cloud Key device.
# It supports:
# - self-hosted Unifi Controller
# - Unifi Cloud Key (Gen1/2/2+)
# - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only)
# - Unifi Dream Machine
# This has not been tested on other "all-in-one" devices such as
# UDM Pro or Unifi Express.
#
# OS Version v2.0.0+
# Network Application version 7.0.0+
# OS version ~3.1 removed java and keytool from the UnifiOS.
# Using PKCS12 format keystore appears to work fine.
#
# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359
#returns 0 means success, otherwise error.
# The deploy-hook automatically detects standard Unifi installations
# for each of the supported environments. Most users should not need
# to set any of these variables, but if you are running a self-hosted
# Controller with custom locations, set these as necessary before running
# the deploy hook. (Defaults shown below.)
#
# Settings for Unifi Controller:
# Location of Java keystore or unifi.keystore.jks file:
#DEPLOY_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
# Keystore password (built into Unifi Controller, not a user-set password):
#DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
# Command to restart Unifi Controller:
# DEPLOY_UNIFI_RELOAD="systemctl restart unifi"
# System Properties file location for controller
#DEPLOY_UNIFI_SYSTEM_PROPERTIES="/usr/lib/unifi/data/system.properties"
#
# Settings for Unifi Cloud Key Gen1 (nginx admin pages):
# Directory where cloudkey.crt and cloudkey.key live:
#DEPLOY_UNIFI_CLOUDKEY_CERTDIR="/etc/ssl/private"
# Command to restart maintenance pages and Controller
# (same setting as above, default is updated when running on Cloud Key Gen1):
#DEPLOY_UNIFI_RELOAD="service nginx restart && service unifi restart"
#
# Settings for UnifiOS (Cloud Key Gen2):
# Directory where unifi-core.crt and unifi-core.key live:
#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
# Command to restart unifi-core:
# DEPLOY_UNIFI_OS_RELOAD="systemctl restart unifi-core"
#
# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
unifi_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_UNIFI_KEYSTORE
_getdeployconf DEPLOY_UNIFI_KEYPASS
_getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
_getdeployconf DEPLOY_UNIFI_CORE_CONFIG
_getdeployconf DEPLOY_UNIFI_RELOAD
_getdeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES
_getdeployconf DEPLOY_UNIFI_OS_RELOAD
_debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
_debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
_debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
_debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
_debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
_debug2 DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
_debug2 DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
# Space-separated list of environments detected and installed:
_services_updated=""
# Default reload commands accumulated as we auto-detect environments:
_reload_cmd=""
# Unifi Controller environment (self hosted or any Cloud Key) --
# auto-detect by file /usr/lib/unifi/data/keystore
_unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}"
if [ -f "$_unifi_keystore" ]; then
_debug _unifi_keystore "$_unifi_keystore"
if ! _exists keytool; then
_do_keytool=0
_info "Installing certificate for Unifi Controller (PKCS12 keystore)."
else
_do_keytool=1
_info "Installing certificate for Unifi Controller (Java keystore)"
fi
if [ ! -w "$_unifi_keystore" ]; then
_err "The file $_unifi_keystore is not writable, please change the permission."
return 1
fi
_unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-aircontrolenterprise}"
_debug "Generate import pkcs12"
_import_pkcs12="$(_mktemp)"
_debug "_toPkcs $_import_pkcs12 $_ckey $_ccert $_cca $_unifi_keypass unifi root"
_toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
# shellcheck disable=SC2181
if [ "$?" != "0" ]; then
_err "Error generating pkcs12. Please re-run with --debug and report a bug."
return 1
fi
# Save the existing keystore in case something goes wrong.
mv -f "${_unifi_keystore}" "${_unifi_keystore}"_original
_info "Previous keystore saved to ${_unifi_keystore}_original."
if [ "$_do_keytool" -eq 1 ]; then
_debug "Import into keystore: $_unifi_keystore"
if keytool -importkeystore \
-deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
-srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
-alias unifi -noprompt; then
_debug "Import keystore success!"
else
_err "Error importing into Unifi Java keystore."
_err "Please re-run with --debug and report a bug."
_info "Restoring original keystore."
mv -f "${_unifi_keystore}"_original "${_unifi_keystore}"
rm "$_import_pkcs12"
return 1
fi
else
_debug "Copying new keystore to $_unifi_keystore"
cp -f "$_import_pkcs12" "$_unifi_keystore"
fi
# correct file ownership according to the directory, the keystore is placed in
_unifi_keystore_dir=$(dirname "${_unifi_keystore}")
# shellcheck disable=SC2012
_unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}')
# shellcheck disable=SC2012
_unifi_keystore_owner=$(ls -l "${_unifi_keystore}" | awk '{print $3}')
if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then
_debug "Changing keystore owner to ${_unifi_keystore_dir_owner}"
chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root
fi
# Update unifi service for certificate cipher compatibility
_unifi_system_properties="${DEPLOY_UNIFI_SYSTEM_PROPERTIES:-/usr/lib/unifi/data/system.properties}"
if ${ACME_OPENSSL_BIN:-openssl} pkcs12 \
-in "$_import_pkcs12" \
-password pass:aircontrolenterprise \
-nokeys | ${ACME_OPENSSL_BIN:-openssl} x509 -text \
-noout | grep -i "signature" | grep -iq ecdsa >/dev/null 2>&1; then
if [ -f "$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" ]; then
_unifi_system_properties="$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties"
else
_unifi_system_properties="/usr/lib/unifi/data/system.properties"
fi
if [ -f "${_unifi_system_properties}" ]; then
cp -f "${_unifi_system_properties}" "${_unifi_system_properties}"_original
_info "Updating system configuration for cipher compatibility."
_info "Saved original system config to ${_unifi_system_properties}_original"
sed -i '/unifi\.https\.ciphers/d' "${_unifi_system_properties}"
echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>"${_unifi_system_properties}"
sed -i '/unifi\.https\.sslEnabledProtocols/d' "${_unifi_system_properties}"
echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>"${_unifi_system_properties}"
_info "System configuration updated."
fi
fi
rm "$_import_pkcs12"
# Restarting unifi-core will bring up unifi, doing it out of order results in
# a certificate error, and breaks wifiman.
# Restart if we aren't doing Unifi OS (e.g. unifi-core service), otherwise stop for later restart.
_unifi_reload="${DEPLOY_UNIFI_RELOAD:-systemctl restart unifi}"
if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
else
_info "Stopping Unifi Controller for later restart."
_unifi_stop=$(echo "${_unifi_reload}" | sed -e 's/restart/stop/')
$_unifi_stop
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
_info "Unifi Controller stopped."
fi
_services_updated="${_services_updated} unifi"
_info "Install Unifi Controller certificate success!"
elif [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
_err "The specified DEPLOY_UNIFI_KEYSTORE='$DEPLOY_UNIFI_KEYSTORE' is not valid, please check."
return 1
fi
# Cloud Key environment (non-UnifiOS -- nginx serves admin pages) --
# auto-detect by file /etc/ssl/private/cloudkey.key:
_cloudkey_certdir="${DEPLOY_UNIFI_CLOUDKEY_CERTDIR:-/etc/ssl/private}"
if [ -f "${_cloudkey_certdir}/cloudkey.key" ]; then
_info "Installing certificate for Cloud Key Gen1 (nginx admin pages)"
_debug _cloudkey_certdir "$_cloudkey_certdir"
if [ ! -w "$_cloudkey_certdir" ]; then
_err "The directory $_cloudkey_certdir is not writable; please check permissions."
return 1
fi
# Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
# It appears that unifi won't start if this is a symlink, so we'll copy it instead.
# if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
# _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
# return 1
# fi
_info "Updating ${_cloudkey_certdir}/unifi.keystore.jks"
if [ -e "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
if [ -L "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
rm -f "${_cloudkey_certdir}/unifi.keystore.jks"
else
mv "${_cloudkey_certdir}/unifi.keystore.jks" "${_cloudkey_certdir}/unifi.keystore.jks_original"
fi
fi
cp "${_unifi_keystore}" "${_cloudkey_certdir}/unifi.keystore.jks"
cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
(cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
if systemctl -q is-active nginx; then
_reload_cmd="${_reload_cmd:+$_reload_cmd && }service nginx restart"
fi
_info "Install Cloud Key Gen1 certificate success!"
_services_updated="${_services_updated} nginx"
elif [ "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" ]; then
_err "The specified DEPLOY_UNIFI_CLOUDKEY_CERTDIR='$DEPLOY_UNIFI_CLOUDKEY_CERTDIR' is not valid, please check."
return 1
fi
# UnifiOS environment -- auto-detect by /data/unifi-core/config/unifi-core.key:
_unifi_core_config="${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}"
if [ -f "${_unifi_core_config}/unifi-core.key" ]; then
_info "Installing certificate for UnifiOS"
_debug _unifi_core_config "$_unifi_core_config"
if [ ! -w "$_unifi_core_config" ]; then
_err "The directory $_unifi_core_config is not writable; please check permissions."
return 1
fi
# Save the existing certs in case something goes wrong.
cp -f "${_unifi_core_config}"/unifi-core.crt "${_unifi_core_config}"/unifi-core_original.crt
cp -f "${_unifi_core_config}"/unifi-core.key "${_unifi_core_config}"/unifi-core_original.key
_info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt.key."
cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt"
cat "$_ckey" >"${_unifi_core_config}/unifi-core.key"
_unifi_os_reload="${DEPLOY_UNIFI_OS_RELOAD:-systemctl restart unifi-core}"
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_os_reload"
_info "Install UnifiOS certificate success!"
_services_updated="${_services_updated} unifi-core"
elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
_err "The specified DEPLOY_UNIFI_CORE_CONFIG='$DEPLOY_UNIFI_CORE_CONFIG' is not valid, please check."
return 1
fi
if [ -z "$_services_updated" ]; then
# None of the Unifi environments were auto-detected, so no deployment has occurred
# (and none of DEPLOY_UNIFI_{KEYSTORE,CLOUDKEY_CERTDIR,CORE_CONFIG} were set).
_err "Unable to detect Unifi environment in standard location."
_err "(This deploy hook must be run on the Unifi device, not a remote machine.)"
_err "For non-standard Unifi installations, set DEPLOY_UNIFI_KEYSTORE,"
_err "DEPLOY_UNIFI_CLOUDKEY_CERTDIR, and/or DEPLOY_UNIFI_CORE_CONFIG as appropriate."
return 1
fi
_reload_cmd="${DEPLOY_UNIFI_RELOAD:-$_reload_cmd}"
if [ -z "$_reload_cmd" ]; then
_err "Certificates were installed for services:${_services_updated},"
_err "but none appear to be active. Please set DEPLOY_UNIFI_RELOAD"
_err "to a command that will restart the necessary services."
return 1
fi
_info "Reload services (this may take some time): $_reload_cmd"
if eval "$_reload_cmd"; then
_info "Reload success!"
else
_err "Reload error"
return 1
fi
# Successful, so save all (non-default) config:
_savedeployconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
_savedeployconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
_savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
_savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
_savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
_savedeployconf DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
_savedeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
return 0
}
-341
View File
@@ -1,341 +0,0 @@
#!/usr/bin/env sh
# Deploy hook for UniFi OS, via the certificate REST API.
#
# Works against any UniFi OS whose management UI exposes
# /api/userCertificates. Confirmed on:
# - UniFi OS Server (the separately-installed, self-hosted application)
# on macOS and on Linux. Windows should also work (it runs under
# WSL2), but has not been tested.
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
# - UniFi OS hardware: UDM Pro on UniFi OS 5.1.26, UCG Fiber on
# UniFi OS 5.0.16 (user reports, see issues 7184 and 6916).
# No lower version bound is claimed -- if the UI has a certificate
# manager, this hook should work.
#
# `unifios` vs `unifi`: the split is the access method, not the product
# line. `unifi` writes files / a Java keystore and needs local or SSH
# access on the device; this hook drives the same REST API the web UI
# uses and works remotely. Use `unifi` where acme.sh runs on the device
# itself, this hook where it does not.
#
# The API is served on the management port, which differs per install:
# UniFi OS Server listens on 11443 (hence the default below), while
# UniFi OS hardware serves it on 443 -- set DEPLOY_UNIFIOS_HOST to
# "https://<host>" there.
#
# Endpoints used, all as the web UI itself calls them:
# POST /api/auth/login - session login (cookie + JWT)
# GET /api/userCertificates - list uploaded certificates
# POST /api/userCertificates - upload a new certificate
# DELETE /api/userCertificates/{id} - remove a certificate
# PUT /api/userCertificates/{id}/status - activate/deactivate a certificate
#
# This was reverse-engineered from the browser's Network tab while using the
# real GUI upload/activate/delete flow -- it is undocumented but is the same
# code path the UI uses, so it's far more robust than editing settings.yaml,
# http/local-certs.conf, or the underlying Postgres user_certificates table
# directly (all of which are also touched by this API, but only as a result
# of the app's own internal logic, which handles cert parsing, active-cert
# bookkeeping, and nginx config regeneration correctly on its own).
#
# Auth: POST /api/auth/login returns a `TOKEN` cookie containing a JWT whose
# payload has a `csrfToken` claim. That value must be echoed back as the
# `x-csrf-token` header on every subsequent state-changing request (a classic
# double-submit CSRF pattern). No other cookies were found to be necessary.
#
# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
# honored the same as every other hook. The management API's cert may be
# self-signed -- it always is on a fresh install, and there is no reliable
# way to tell in advance whether an earlier run has already replaced it --
# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
# verification for the rest of the acme.sh run, e.g. the connection to the
# ACME CA.
#
# Design: This hook does not save a certificate ID between renewals. Each
# upload gets a name unique to that run: the domain name plus a timestamp.
# This name never collides with an entry from a previous deploy. This is
# true even if that entry is still active. The hook uploads and activates
# the new certificate before it removes any old entries. If a failure
# occurs during this process, the server still has a valid, active
# certificate. The hook removes old entries only after activation is
# complete. It removes only entries whose name starts with the domain name,
# because this is the hook's own naming convention. As a result, this step
# can only affect entries that this hook created for this domain. It can
# never affect a certificate that a user uploaded manually, and it can
# never affect a self-signed certificate.
#
# Settings:
# DEPLOY_UNIFIOS_HOST - base URL of the management API
# (default: "https://localhost:11443", i.e. a UniFi OS Server on the
# same machine as acme.sh; set it to "https://<host>" for UniFi OS
# hardware or any remote target)
# DEPLOY_UNIFIOS_USERNAME - UniFi OS admin username (required)
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS admin password (required)
#
# Example:
# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
# export DEPLOY_UNIFIOS_PASSWORD="xxxxx"
# acme.sh --deploy -d example.com --deploy-hook unifios
#
# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/7182
_uos_response_code() {
# tr strips the trailing newline along with form feeds; re-terminate
# before the second _egrep_o, whose sed fallback (used wherever egrep -o
# is unavailable) drops an unterminated final line on some platforms.
_uos_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
printf '%s\n' "$_uos_code" | _egrep_o "^[0-9][0-9]*"
}
_uos_response_cookie() {
# $1 = cookie name
grep <"$HTTP_HEADER" -i "^Set-Cookie: *$1=" | _tail_n 1 | _egrep_o "$1=[^;]*" | _head_n 1
}
_uos_split_json() {
# $1 = raw JSON list response
#
# _normalizeJson collapses the response to one line. This removes extra
# space around colons. It also removes any CR or LF characters that the
# server can add. However, _normalizeJson also removes the newline
# character at the end of the line. If the line has no ending newline
# character, some sed programs drop the last line of input. This code
# adds the newline back before the split below, to prevent that problem.
_uos_normalized="$(echo "$1" | _normalizeJson)"
# A literal newline character splits the JSON into one object per line.
# Grep can then match a single certificate entry at a time. This is not
# the two-character "\n" sequence: GNU sed reads "\n" in the replacement
# text as a newline character. POSIX does not define this behavior, and
# BSD sed prints "\n" as two literal characters, not as a newline.
printf '%s\n' "$_uos_normalized" | sed 's/},{/},\
{/g'
}
_uos_grep_literal() {
# $1 = literal text to find, matched without a regex -- portable to
# grep implementations with no -F flag (e.g. Solaris), and avoids "*"
# or "." in a domain name being read as a regex metacharacter.
while IFS= read -r _uos_line || [ -n "$_uos_line" ]; do
case "$_uos_line" in
*"$1"*) printf '%s\n' "$_uos_line" ;;
esac
done
}
unifios_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Scoped to this hook's own subshell -- does not affect the rest of the
# acme.sh run (e.g. the connection to the ACME CA).
export HTTPS_INSECURE=1
_getdeployconf DEPLOY_UNIFIOS_HOST
DEPLOY_UNIFIOS_HOST="${DEPLOY_UNIFIOS_HOST:-https://localhost:11443}"
_savedeployconf DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
_debug DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
_getdeployconf DEPLOY_UNIFIOS_USERNAME
_getdeployconf DEPLOY_UNIFIOS_PASSWORD
if [ -z "$DEPLOY_UNIFIOS_USERNAME" ] || [ -z "$DEPLOY_UNIFIOS_PASSWORD" ]; then
_err "DEPLOY_UNIFIOS_USERNAME and DEPLOY_UNIFIOS_PASSWORD must be set."
return 1
fi
_debug DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME"
_secure_debug DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD"
_info "Logging in to UniFi OS Server API at $DEPLOY_UNIFIOS_HOST..."
# _json_encode always appends a trailing "\n" escape, even to input with
# no trailing newline (it normalizes via `echo`, unconditionally adding
# one). That's harmless for the key/cert file content below, which
# legitimately ends in a real newline anyway, but wrong for these plain
# strings -- strip the spurious escape it leaves behind.
_uos_user_json="$(printf '%s' "$DEPLOY_UNIFIOS_USERNAME" | _json_encode)"
_uos_user_json="${_uos_user_json%\\n}"
_uos_pass_json="$(printf '%s' "$DEPLOY_UNIFIOS_PASSWORD" | _json_encode)"
_uos_pass_json="${_uos_pass_json%\\n}"
_login_body="{\"username\":\"$_uos_user_json\",\"password\":\"$_uos_pass_json\",\"token\":\"\",\"rememberMe\":false}"
_login_json="$(_post "$_login_body" "$DEPLOY_UNIFIOS_HOST/api/auth/login" "" "POST" "application/json")"
_login_code="$(_uos_response_code)"
if [ "$_login_code" != "200" ]; then
_err "Login failed (HTTP $_login_code)."
_err "Response: $_login_json"
return 1
fi
# Credentials are proven correct now -- save them, rather than only at the
# very end, so a later step failing doesn't discard a working login.
# base64-encoded: _save_conf wraps values in single quotes with no
# escaping, so a literal "'" in the password would otherwise corrupt the
# domain conf (see deploy/synology_dsm.sh for the same pattern).
_savedeployconf DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" "base64"
_savedeployconf DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" "base64"
_uos_token="$(_uos_response_cookie TOKEN)"
if [ -z "$_uos_token" ]; then
_err "Login succeeded but no TOKEN cookie was returned."
return 1
fi
_H1="Cookie: $_uos_token"
export _H1
_uos_jwt_payload="$(echo "$_uos_token" | cut -d '=' -f 2- | cut -d '.' -f 2)"
_uos_csrf="$(_durl_replace_base64 "$_uos_jwt_payload" | _dbase64 | _egrep_o '"csrfToken":"[^"]*"' | cut -d '"' -f 4)"
if [ -z "$_uos_csrf" ]; then
_err "Could not extract csrfToken from session token."
return 1
fi
_H2="x-csrf-token: $_uos_csrf"
export _H2
_info "Uploading new certificate..."
# "name" is a purely cosmetic label -- the server never validates it
# against the certificate's actual CN/SAN, and accepts arbitrary text
# including spaces (confirmed: a cert for example.com served correctly
# after being uploaded under the unrelated name "totally unrelated label").
# The only constraint that matters here is uniqueness: the server rejects
# a second entry with a name it already has, so a bare domain name would
# collide with the previous deploy's entry on every renewal after the
# first. A full human-readable timestamp would make that obvious in the
# UI, but the certificate list's name column is fixed-width and doesn't
# wrap (confirmed against the real UI: a long name overlaps the Expires
# column and makes both unreadable), so keep the suffix short instead --
# Unix epoch seconds are still unique enough for this purpose.
#
# This name includes the key type (rsa or ecdsa), to keep an RSA
# deploy and an ECC deploy of the same domain from sharing this
# prefix. Without the key type, the cleanup step for each deploy
# removes the entry that the other deploy creates. `deploy/haproxy.sh`
# and `deploy/lighttpd.sh` use the same `_isEccKey` check, for the same
# reason.
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
if _isEccKey "${Le_Keylength}"; then
_uos_keytype="ecdsa"
else
_uos_keytype="rsa"
fi
_uos_name="$_cdomain $_uos_keytype $(_time)"
_uos_key_json="$(_json_encode <"$_ckey")"
_uos_cert_json="$(_json_encode <"$_cfullchain")"
_create_body="{\"name\":\"$_uos_name\",\"key\":\"$_uos_key_json\",\"cert\":\"$_uos_cert_json\"}"
_create_json="$(_post "$_create_body" "$DEPLOY_UNIFIOS_HOST/api/userCertificates" "" "POST" "application/json")"
_create_code="$(_uos_response_code)"
if [ "$_create_code" = "201" ]; then
_new_id="$(echo "$_create_json" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_new_id" ]; then
_err "Could not determine new certificate ID from upload response."
return 1
fi
elif [ "$_create_code" = "400" ] && echo "$_create_json" | grep -q "USER_CERTIFICATE_DUPLICATE"; then
# HTTP 400 alone just means "bad request" -- it's the USER_CERTIFICATE_DUPLICATE
# code in the response body, checked above, that actually confirms this.
# The name above is unique to this run, so a duplicate here can only be
# the server's other uniqueness constraint: this exact certificate (by
# fingerprint) already exists as some other entry -- most likely a retry
# after a prior run already uploaded it (a real renewal always produces a
# new fingerprint, so this shouldn't happen in normal cron use). The
# response body doesn't include the existing entry's id, so look it up
# by fingerprint instead.
# The API's own fingerprint field is SHA-1 (20 bytes), not SHA-256 --
# confirmed against a real response, e.g.
# "fingerprint":"FC:02:50:9C:3B:3F:B7:79:9D:CA:4D:7C:AC:92:E7:D5:EA:F1:3A:29"
# (20 colon-separated groups). _fingerprint (core helper) strips the
# colons that field has, so re-insert them rather than stripping the
# JSON's own colons, which would also remove the ones separating every
# key from its value.
_uos_fingerprint="$(_fingerprint "$_cfullchain" sha1)"
if [ -z "$_uos_fingerprint" ]; then
_err "Could not compute the certificate's fingerprint."
return 1
fi
_uos_fingerprint="$(echo "$_uos_fingerprint" | sed 's/\(..\)/\1:/g; s/:$//')"
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
_list_code="$(_uos_response_code)"
if [ "$_list_code" != "200" ]; then
_err "Failed to list existing certificates (HTTP $_list_code)."
_err "Response: $_list_json"
return 1
fi
_list_json="$(_uos_split_json "$_list_json")"
_new_id="$(echo "$_list_json" | _uos_grep_literal "\"fingerprint\":\"$_uos_fingerprint\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_new_id" ]; then
_err "Certificate upload rejected as a duplicate (server reported USER_CERTIFICATE_DUPLICATE), but no existing entry matching this fingerprint was found."
_err "Response: $_create_json"
return 1
fi
# Reusing the existing entry rather than deleting it and re-uploading
# under today's name+timestamp: the served content is identical either
# way, so replacing it would only cost an extra delete+create round trip
# for no functional benefit. The tradeoff is cosmetic -- this entry keeps
# whatever name it was given whenever it was originally uploaded, so it
# won't reflect today's date in the UI.
_info "Certificate already present as entry $_new_id; reusing it."
else
_err "Certificate upload failed (HTTP $_create_code)."
_err "Response: $_create_json"
return 1
fi
_info "Activating certificate $_new_id..."
_activate_json="$(_post '{"active":true}' "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_new_id/status" "" "PUT" "application/json")"
_activate_code="$(_uos_response_code)"
if [ "$_activate_code" != "200" ]; then
_err "Failed to activate new certificate (HTTP $_activate_code)."
_err "Response: $_activate_json"
return 1
fi
# UniFi OS Server activation is exclusive server-wide. Tests against the
# real API confirm this: activation of one entry deactivates whichever
# other entry was active before, no matter its name or domain. As a
# result, the server serves the certificate that this hook just activated.
# This certificate is already live. If the removal of old entries below
# fails, the hook logs the failure. The deploy does not fail because of
# this.
_info "Checking for old certificate entries to remove..."
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
_list_code="$(_uos_response_code)"
if [ "$_list_code" != "200" ]; then
_err "Failed to list certificates for cleanup (HTTP $_list_code) -- leaving old entries in place."
else
_list_json="$(_uos_split_json "$_list_json")"
# The pattern below matches the domain name and key type, followed by
# a space. If the space is missing, the pattern can also match a
# different domain that starts with the same text as this domain.
_old_ids="$(echo "$_list_json" | _uos_grep_literal "\"name\":\"$_cdomain $_uos_keytype " | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4 | grep -v "^$_new_id$")"
for _old_id in $_old_ids; do
_info "Removing old certificate entry $_old_id..."
_del_json="$(_post "" "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_old_id" "" "DELETE")"
_del_code="$(_uos_response_code)"
if [ "$_del_code" != "204" ] && [ "$_del_code" != "200" ]; then
_err "Failed to delete old certificate $_old_id (HTTP $_del_code) -- leaving it in place."
_err "Response: $_del_json"
fi
done
fi
_info "UniFi OS Server certificate deployed and activated successfully."
return 0
}
Loaded 100 of 333 files, more files were not shown because too many files have changed in this diff. Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.