├── LICENSE ├── README.md └── action.yml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Menci 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action for acme.sh 2 | 3 | Issue SSL certificate with acme.sh's DNS API mode. 4 | 5 | You probably want to use this action in a private repo, to upload your issued SSL certificate to repo. 6 | 7 | # Usage 8 | 9 | ```yaml 10 | jobs: 11 | issue-ssl-certificate: 12 | name: Issue SSL certificate 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: Menci/acme@v1 16 | with: 17 | version: 3.0.2 18 | 19 | # Register your account and try issue a certificate with DNS API mode 20 | # Then fill with the output of `tar cz ca account.conf | base64 -w0` running in your `~/.acme.sh` 21 | account-tar: ${{ secrets.ACME_SH_ACCOUNT_TAR }} 22 | 23 | domains: example.com example.net example.org example.edu 24 | domains-file: '' 25 | append-wildcard: true 26 | 27 | arguments: --dns dns_cf --challenge-alias example.com 28 | arguments-file: '' 29 | 30 | output-cert: output/cert.pem 31 | output-fullchain: output/fullchain.pem 32 | output-key: output/key.pem 33 | output-pfx: output/certificate.pfx 34 | output-pfx-password: qwq 35 | 36 | # uninstall: true # Uninstall acme.sh after this action by default 37 | ``` 38 | 39 | # Notice 40 | 41 | If you issue a certificate with too many domains with DNS alias mode. The TXT records' length will likely exceed the DNS provider's limit and fails ([acmesh-official/acme.sh#3748](https://github.com/acmesh-official/acme.sh/issues/3748)). To workaround this, this action will run acme.sh **multiple times** and issue a smaller certificate each time (so we can verify a smaller amount of domains each time). The result certificate will be fine. 42 | 43 | # See also 44 | 45 | You can deploy your newly issued SSL certificate to Azure Web App with the [Deploy SSL certificate to Azure Web App](https://github.com/marketplace/actions/deploy-ssl-certificate-to-azure-web-app) action. 46 | 47 | You can deploy your newly issued SSL certificate to Aliyun Certificates Service and Aliyun CDN with the [Deploy SSL certificate to Aliyun](https://github.com/marketplace/actions/deploy-ssl-certificate-to-aliyun) action. 48 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Issue SSL certificate 2 | description: Issue SSL certificate with acme.sh's DNS API mode. 3 | branding: 4 | icon: lock 5 | color: green 6 | inputs: 7 | version: 8 | description: The version of acme.sh. By default the latest version is used. 9 | required: false 10 | default: '' 11 | account-conf-content: 12 | description: (Deprecated, please use "account-tar") The text content of your `account.conf`, should be stored in secrets. If not specfied you should provide your DNS API token with environment variables. 13 | required: false 14 | default: '' 15 | account-tar: 16 | description: Base64 encoded tar file content of your account files in `~/.acme.sh` (i.e. output of `tar cz ca account.conf | base64 -w0` in your `~/.acme.sh` directory). 17 | required: false 18 | default: '' 19 | domains: 20 | description: The list of domains you want to issue certificate for. Separated by any blank characters (allowing newlines). Overrided by `domains-file` field. 21 | required: false 22 | default: '' 23 | domains-file: 24 | description: The file containing a list of domains you want to issue certificate for. Separated by any blank characters (allowing newlines). Overrides `domains` field. 25 | required: false 26 | default: '' 27 | append-wildcard: 28 | description: Whether to add a wildcard entry for each of your domain in `domains`. 29 | required: false 30 | default: true 31 | arguments: 32 | description: The arguments to pass to acme.sh (will be prepended to all `-d domain.name` items). The first argument `--issue` should be omitted. For example `--dns dns_cf --challenge-alias example.com`. Overrided by `arguments-file` field. 33 | required: false 34 | default: '' 35 | arguments-file: 36 | description: The file containing arguments to pass to acme.sh (will be prepended to all `-d domain.name` items). The first argument `--issue` should be omitted. For example `--dns dns_cf --challenge-alias example.com`. Overrides `arguments` field. 37 | required: false 38 | default: '' 39 | output-cert: 40 | description: The target path for the issued certificate's cert PEM file. Omit if you don't need. 41 | required: false 42 | default: '' 43 | output-fullchain: 44 | description: The target path for the issued certificate's fullchain PEM file. Omit if you don't need. 45 | required: false 46 | default: '' 47 | output-key: 48 | description: The target path for the issued certificate's private key PEM file. Omit if you don't need. 49 | required: false 50 | default: '' 51 | output-pfx: 52 | description: The target path for the issued certificate's PKCS#12 certificate file. Please also specify the `output-pfx-password` option. Omit if you don't need. 53 | required: false 54 | default: '' 55 | output-pfx-password: 56 | description: The password for the output PKCS#12 certificate file. Ignored when `output-pfx` is not specfied. 57 | required: false 58 | default: '' 59 | uninstall: 60 | description: Whether or not to uninstall acme.sh after running this action. Use `false` to keep. 61 | required: false 62 | default: true 63 | runs: 64 | using: "composite" 65 | steps: 66 | - name: Install acme.sh 67 | run: curl https://get.acme.sh | sh 68 | shell: bash 69 | env: 70 | BRANCH: ${{ inputs.version }} 71 | - name: Extract account files for acme.sh 72 | run: | 73 | if ! [[ -z "$ACME_SH_INPUT_ACCOUNT_CONF_CONTENT" ]]; then 74 | echo "$ACME_SH_INPUT_ACCOUNT_CONF_CONTENT" > ~/.acme.sh/account.conf 75 | fi 76 | 77 | if ! [[ -z "$ACME_SH_INPUT_ACCOUNT_TAR" ]]; then 78 | echo "$ACME_SH_INPUT_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz 79 | fi 80 | shell: bash 81 | env: 82 | ACME_SH_INPUT_ACCOUNT_CONF_CONTENT: ${{ inputs.account-conf-content }} 83 | ACME_SH_INPUT_ACCOUNT_TAR: ${{ inputs.account-tar }} 84 | - name: Preprocess domain list 85 | run: | 86 | if ! [[ -z "$ACME_SH_INPUT_DOMAINS_FILE" ]]; then 87 | ACME_SH_INPUT_DOMAINS="$(cat "$ACME_SH_INPUT_DOMAINS_FILE")" 88 | fi 89 | ACME_SH_DOMAINS="$(echo "$ACME_SH_INPUT_DOMAINS" | xargs)" 90 | ACME_SH_FIRST_DOMAIN="$(cut -d ' ' -f1 <<< "$ACME_SH_DOMAINS")" 91 | if [[ -z "$ACME_SH_FIRST_DOMAIN" ]]; then 92 | echo 'No domains!' 93 | exit 1 94 | fi 95 | ACME_SH_DOMAIN_REGEX='([A-Za-z0-9\.\*-]+)' 96 | if [[ "$ACME_SH_APPEND_WILDCARD" == "true" ]]; then 97 | ACME_SH_DOMAINS="$(sed -E "s/$ACME_SH_DOMAIN_REGEX/\1 *.\1/g" <<< "$ACME_SH_DOMAINS")" 98 | fi 99 | # Strip domains included in wildcards 100 | ACME_SH_DOMAINS="$(python3 -c "import sys; l = sys.stdin.read().strip().split(' '); l1 = [s.split('.') for s in l]; l2 = ['.'.join(x) for x in l1 if x[0] == '*' or ['*'] + x[1:] not in l1]; print(' '.join(l2))" <<< "$ACME_SH_DOMAINS")" 101 | ACME_SH_ARGS_DOMAIN_LINES="$(sed -E "s/$ACME_SH_DOMAIN_REGEX/-d '\1'/g" <<< "$ACME_SH_DOMAINS" | tr " " "\n")" 102 | # Workaround actions#runner/789 103 | ACME_SH_GITHUB_ENV_CONTENT="$(cat "$GITHUB_ENV")" 104 | echo "$ACME_SH_GITHUB_ENV_CONTENT" | grep -v "^ACME_SH_FIRST_DOMAIN=" | grep -v "^ACME_SH_ARGS_DOMAINS=" > "$GITHUB_ENV" 105 | echo "ACME_SH_FIRST_DOMAIN=$ACME_SH_FIRST_DOMAIN" >> "$GITHUB_ENV" 106 | echo "ACME_SH_ARGS_DOMAIN_LINES<> "$GITHUB_ENV" 107 | echo "$ACME_SH_ARGS_DOMAIN_LINES" >> "$GITHUB_ENV" 108 | echo "EOF" >> "$GITHUB_ENV" 109 | shell: bash 110 | env: 111 | ACME_SH_INPUT_DOMAINS: ${{ inputs.domains }} 112 | ACME_SH_INPUT_DOMAINS_FILE: ${{ inputs.domains-file }} 113 | ACME_SH_APPEND_WILDCARD: ${{ inputs.append-wildcard }} 114 | - name: Issue certificate 115 | run: | 116 | if ! [[ -z "$ACME_SH_INPUT_ARGS_PREPENDED_FILE" ]]; then 117 | ACME_SH_INPUT_ARGS_PREPENDED="$(cat "$ACME_SH_INPUT_ARGS_PREPENDED_FILE")" 118 | fi 119 | if [[ -z "$ACME_SH_INPUT_ARGS_PREPENDED" ]]; then 120 | echo 'No arguments!' 121 | exit 1 122 | fi 123 | 124 | function acme_sh_issue() { 125 | echo "$ACME_SH_INPUT_ARGS_PREPENDED" "$1" | xargs ~/.acme.sh/acme.sh --issue 126 | } 127 | 128 | if ! acme_sh_issue "$ACME_SH_ARGS_DOMAIN_LINES"; then 129 | # Workaround acmesh-official/acme.sh#3748 130 | ACME_SH_ISSUE_BATCH_SIZE=10 131 | ACME_SH_TOTAL_DOMAINS="$(wc -l <<< "$ACME_SH_ARGS_DOMAIN_LINES")" 132 | for ((i = ACME_SH_ISSUE_BATCH_SIZE; i - ACME_SH_ISSUE_BATCH_SIZE < ACME_SH_TOTAL_DOMAINS; i += ACME_SH_ISSUE_BATCH_SIZE)); do 133 | ACME_SH_ARGS_DOMAINS="$(echo "$ACME_SH_ARGS_DOMAIN_LINES" | head -n "$i" | xargs)" 134 | acme_sh_issue "$ACME_SH_ARGS_DOMAINS" 135 | 136 | if [[ "$i" != "$ACME_SH_TOTAL_DOMAINS" ]]; then 137 | echo Sleeping for 30s before next attempt. 138 | sleep 30 139 | fi 140 | done 141 | fi 142 | shell: bash 143 | env: 144 | ACME_SH_INPUT_ARGS_PREPENDED: ${{ inputs.arguments }} 145 | ACME_SH_INPUT_ARGS_PREPENDED_FILE: ${{ inputs.arguments-file }} 146 | - name: Copy certificate to output paths 147 | run: | 148 | ACME_SH_TEMP_DIR="$(mktemp -d)" 149 | ACME_SH_TEMP_FILE_CERT="$ACME_SH_TEMP_DIR/cert.pem" 150 | ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem" 151 | ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem" 152 | ~/.acme.sh/acme.sh --install-cert -d "$ACME_SH_FIRST_DOMAIN" --cert-file "$ACME_SH_TEMP_FILE_CERT" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY" 153 | [[ -z "$ACME_SH_OUTPUT_CERT" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_CERT")" && cp "$ACME_SH_TEMP_FILE_CERT" "$ACME_SH_OUTPUT_CERT") 154 | [[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN") 155 | [[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY") 156 | [[ -z "$ACME_SH_OUTPUT_PFX" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_PFX")" && ( 157 | OPENSSL_ARGS_EXTRA="" 158 | if openssl pkcs12 --help 2>&1 | grep "\-legacy" >/dev/null; then 159 | OPENSSL_ARGS_EXTRA="-legacy" 160 | fi 161 | openssl pkcs12 $OPENSSL_ARGS_EXTRA -export -out "$ACME_SH_OUTPUT_PFX" -in "$ACME_SH_TEMP_FILE_FULLCHAIN" -inkey "$ACME_SH_TEMP_FILE_KEY" -password "pass:$ACME_SH_OUTPUT_PFX_PASSWORD" 162 | )) 163 | rm -rf "$ACME_SH_TEMP_DIR" 164 | shell: bash 165 | env: 166 | ACME_SH_OUTPUT_CERT: ${{ inputs.output-cert }} 167 | ACME_SH_OUTPUT_FULLCHAIN: ${{ inputs.output-fullchain }} 168 | ACME_SH_OUTPUT_KEY: ${{ inputs.output-key }} 169 | ACME_SH_OUTPUT_PFX: ${{ inputs.output-pfx }} 170 | ACME_SH_OUTPUT_PFX_PASSWORD: ${{ inputs.output-pfx-password }} 171 | - name: Uninstall acme.sh 172 | if: ${{ inputs.uninstall != 'false' }} 173 | run: | 174 | ~/.acme.sh/acme.sh --uninstall 175 | rm -rf ~/.acme.sh 176 | shell: bash 177 | --------------------------------------------------------------------------------