From 2594ca517d0e0b945f774d92390289bd1ddcaadd Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 17 Jun 2026 12:52:24 -0400 Subject: [PATCH] fix(ansible): clear error when the managed SSH key can't be decrypted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ansible.ssh_private_key can't be decrypted (app secret key changed), decrypt_secret returns the ciphertext unchanged; the executor was writing that enc:v1: blob as the SSH key file → cryptic "Load key ...: error in libcrypto" → Permission denied. Now: - build_credentials skips a still-encrypted key value (never writes ciphertext as a key file). - start_run broadcasts a plain-language run error: "The managed SSH key could not be decrypted (the app secret key changed). Regenerate it in Settings → Ansible and re-provision the host(s)." The run still fails (no usable key), but the reason is now obvious instead of a libcrypto error. Operator remedy: regenerate the managed key + re-provision. Co-Authored-By: Claude Opus 4.8 (1M context) --- steward/ansible/executor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/steward/ansible/executor.py b/steward/ansible/executor.py index e061930..ac8667e 100644 --- a/steward/ansible/executor.py +++ b/steward/ansible/executor.py @@ -13,6 +13,8 @@ from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING +from steward.core.crypto import is_encrypted as _crypto_is_encrypted + if TYPE_CHECKING: from quart import Quart @@ -133,8 +135,12 @@ def build_credentials(creds: dict | None, tmpdir: str) -> tuple[list[str], list[ files: list[tuple[str, str]] = [] creds = creds or {} + from steward.core.crypto import is_encrypted key = creds.get("ssh_private_key") - if key: + # A still-encrypted value means decryption failed (app secret key changed) — + # writing the ciphertext as a key file just yields a cryptic libcrypto error, + # so skip it; start_run surfaces a clear message instead. + if key and not is_encrypted(key): path = os.path.join(tmpdir, "id_key") files.append((path, key if key.endswith("\n") else key + "\n")) args += ["--private-key", path] @@ -391,6 +397,11 @@ async def start_run( effective_creds = dict(creds) if conn.get("password"): effective_creds.pop("ssh_private_key", None) + elif _crypto_is_encrypted(effective_creds.get("ssh_private_key") or ""): + _broadcast(run_id, + "[run error] The managed SSH key could not be decrypted " + "(the app secret key changed). Regenerate it in " + "Settings → Ansible and re-provision the host(s).") cred_args, cred_files = build_credentials(effective_creds, tmpdir) boot_args, boot_files = build_bootstrap(conn, tmpdir)