acmesh-official / acmesh-official/acme.sh

`ssh` deploy hook fails on first-ever deploy when `DEPLOY_SSH_BACKUP` is enabled (default) - backup step doesn't check whether the source file exists before `cp`

Đang mở
#7,249 4 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Shell
Star
47.6k
Fork
5.7k
Merge trung bình
7 ngày 1 giờ
Pull request đã merge (30 ngày)
16

Mô tả

## acme.sh version
v3.1.5 (from `acme.sh --version`)

Note: this is slightly ahead of the latest formal GitHub Release (3.1.4)
at time of reporting - `acme.sh --upgrade`/fresh installs pull directly
from the `master` branch, where the version string is bumped with
ordinary commits, while formal Releases are tagged less frequently.
Confirmed present on `master` as of this report (see code excerpt
below, fetched directly from `master`).

## Environment
- acme.sh running on host A (management/jump server), deploying to a *different*, remote host B via `--deploy-hook ssh`
- `DEPLOY_SSH_USE_SCP=yes`
- Remote host B: Ubuntu, target directory already exists and is writable by the SSH user

## Summary
On the very first deploy to a given remote destination (i.e. before any certificate file has ever been placed there), the `ssh` deploy hook fails outright. The failure comes from the backup step, which unconditionally attempts to `cp` the *existing* remote certificate file to a backup location before writing the new one - without first checking whether that source file actually exists. Since there is nothing to back up on a first deploy, the `cp` fails, and this aborts the entire deployment, including the write of the new certificate that would otherwise have succeeded.

## Steps to reproduce
1. Configure the `ssh` deploy hook for a remote target that has never received a certificate from this hook before (destination directory exists and is writable, but the target key/cert filenames do not exist yet).
2. Leave `DEPLOY_SSH_BACKUP` at its default (do not set it to `no`).
3. Run:
```
acme.sh --deploy -d example.com --deploy-hook ssh --debug 2
```

## Actual behavior
Deployment fails. Debug output shows the backup `cp` command being submitted and failing because the source file doesn't exist yet, which aborts the whole remote command sequence (including the subsequent copy of the new certificate):

```
[Thu Sep 10 19:46:27 CEST 2026] Backup directories erased after 180 days.
[Thu Sep 10 19:46:27 CEST 2026] Remote commands to execute: mkdir -p /dstpath/to/certs/backup/example.com-backup-2026-09-10-17:46:27; { now="$(date -u +%s)"; for fn in /dstpath/to/certs/backup/example.com-backup*; 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; }; =...
[Thu Sep 10 19:46:27 CEST 2026] Submitting sequence of commands to remote server by ssh -T
[Thu Sep 10 19:46:27 CEST 2026] Remote commands to execute: cp /srcpath/to/certs/example.com.key /dstpath/to/certs/backup/example.com-backup-2026-09-10-17:46:27 >/dev/null;=...
[Thu Sep 10 19:46:27 CEST 2026] Submitting sequence of commands to remote server by ssh -T
cp: cannot stat '/srcpath/to/certs/example.com.key': No such file or directory
[Thu Sep 10 19:46:27 CEST 2026] Error code 1 returned from ssh
[Thu Sep 10 19:46:27 CEST 2026] Error deploying for domain: example.com
[Thu Sep 10 19:46:27 CEST 2026] Error encountered while deploying.
```

## Expected behavior
If the source file to be backed up doesn't exist yet (i.e. this is the first deploy to this destination), the backup step should be skipped rather than treated as a fatal error - there is nothing to back up, which is a normal, valid state, not a failure. The actual certificate deployment should then proceed normally.

## Confirmation this is specifically about file-existence, not SSH/auth/scp
Isolated by testing three scenarios against the same host/config, changing only one variable each time:
1. `DEPLOY_SSH_BACKUP=no` -> succeeds.
2. `DEPLOY_SSH_BACKUP=yes`, destination files already present from a prior successful deploy -> succeeds.
3. `DEPLOY_SSH_BACKUP=yes` (or unset/default), destination files not present (fresh target) -> fails, exactly as shown above.

This rules out SSH authentication, `scp` transport, and file permissions as the cause - the only variable that changes the outcome is whether the backup source file exists yet.

## Root cause (confirmed against source)
In `deploy/ssh.sh`, function `_ssh_deploy()`, the backup step for each
file type is unconditional - it builds the backup `cp` command whenever
`DEPLOY_SSH_BACKUP=yes`, with no check that the source file exists yet:

```sh
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite.
_cmdstr="$_cmdstr cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null;"
```

**This same unguarded pattern is repeated identically for all four
deployable file types** (`DEPLOY_SSH_KEYFILE`, `DEPLOY_SSH_CERTFILE`,
`DEPLOY_SSH_CAFILE`, `DEPLOY_SSH_FULLCHAIN`) - this is systemic across
the whole backup mechanism, not limited to the key file specifically.

## Second, related but distinct bug spotted in the same code
While comparing the four otherwise-parallel backup blocks, the
`FULLCHAIN` block appears to contain a copy-paste error. Every other
block correctly checks `DEPLOY_SSH_MULTI_CALL` at this point to decide
whether to dispatch the command immediately:

```sh
# CERTFILE block (correct):
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then

# FULLCHAIN block (appears incorrect):
if [ "$DEPLOY_SSH_FULLCHAIN" = "yes" ]; then
```

`DEPLOY_SSH_FULLCHAIN` holds a file path, so this condition will
essentially never be true - meaning immediate multi-call dispatch of
the fullchain backup command silently never fires, unlike the
equivalent step for keyfile/certfile/cafile. Flagging this here since
it's in the exact same function and section of code, but it looks like
an independent bug from the missing-existence-check issue above.

## Suggested fix
Guard the backup `cp` with an existence check on the remote side, e.g. changing the submitted command from:
```sh
cp "$_old_file" "$_backupdir" >/dev/null
```
to something like:
```sh
[ -f "$_old_file" ] && cp "$_old_file" "$_backupdir" >/dev/null; true
```
so a missing source file is treated as "nothing to back up" rather than a fatal error.

## Workaround
Setting `DEPLOY_SSH_BACKUP=no` avoids the issue entirely. This is a reasonable workaround where the certificate is already backed up through other means, but shouldn't be necessary just to get a first-ever deploy to succeed.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.