fix(ssh): direct ram key materialization without ssh-agent daemon

This commit is contained in:
v 2026-10-22 20:08:50 +03:00
parent 2f6f633aa1
commit 0e9dec3445
2 changed files with 24 additions and 46 deletions

View file

@ -95,8 +95,7 @@ case "${COMMAND}" in
fi
;;
clean|logout)
rm -f "${RAM_CACHE}"
ssh-add -D 2>/dev/null || true
rm -f "${RAM_CACHE}" /dev/shm/.atlas_core
log_ok "Секреты и SSH-ключи безопасно удалены из RAM."
;;
unseal)
@ -233,10 +232,11 @@ Sovereign C2 Orchestrator (dep)
Управление сессией:
dep start [0-24] Открытие смены (TTL в часах, по дефолту 1ч, 0 = бессрочно)
dep clean Закрытие смены (уничтожение RAM-кэша и сброс ssh-agent)
dep clean Закрытие смены (уничтожение RAM-кэша и ключа SSH)
dep unseal Ручной ансил OpenBao + синк токена в K8s
Исполнение и деплой:
dep up [layer] Сквозной макро-деплой: host, net, tier0, talos, apps, all
dep run <selector> Точечный запуск плейбуков: 06 | 01-03 | 01,03,06
dep run list Каталог всех плейбуков с описанием
dep exec <команда...> Вызов любого бинарника строго внутри RAM-изоляции run_isolated

View file

@ -3,7 +3,7 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
KDBX_PATH="${HOME}/Projects/atlas-secrets.kdbx"
RAM_CACHE="/dev/shm/.atlas_secrets_cache"
SSH_SOCK_FILE="/dev/shm/.ssh-agent.sock"
RAM_KEY="/dev/shm/.atlas_core"
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
@ -15,13 +15,6 @@ log_warn() { echo -e "${YELLOW}⚠${NC} $*"; }
log_err() { echo -e "${RED}✖${NC} $*"; }
ensure_prerequisites() {
# Инициализация сокета ssh-agent в RAM
if [[ ! -S "${SSH_SOCK_FILE}" ]]; then
rm -f "${SSH_SOCK_FILE}"
eval $(ssh-agent -a "${SSH_SOCK_FILE}") >/dev/null
fi
export SSH_AUTH_SOCK="${SSH_SOCK_FILE}"
if ! ip route get 10.0.10.12 2>/dev/null | grep -q "192.168.1.2"; then
log_info "Восстанавливаем маршрут к 10.0.0.0/16 через VyOS (192.168.1.2)..."
sudo ip route replace 10.0.0.0/16 via 192.168.1.2 2>/dev/null || true
@ -35,9 +28,9 @@ ensure_prerequisites() {
git config --global credential."https://git.svo88.space".helper "${target_helper}"
fi
# Нативная привязка IdentityAgent в ~/.ssh/config оператора
if [[ -f "${HOME}/.ssh/config" ]] && ! grep -q "IdentityAgent /dev/shm/.ssh-agent.sock" "${HOME}/.ssh/config"; then
printf "\nHost *\n IdentityAgent /dev/shm/.ssh-agent.sock\n" >> "${HOME}/.ssh/config"
# Нативная привязка IdentityFile для C2-хостов в ~/.ssh/config оператора
if [[ -f "${HOME}/.ssh/config" ]] && ! grep -q "IdentityFile /dev/shm/.atlas_core" "${HOME}/.ssh/config"; then
printf "\nHost proxmox pve0 vyos vyos_router 10.0.*\n IdentityFile /dev/shm/.atlas_core\n IdentitiesOnly yes\n" >> "${HOME}/.ssh/config"
fi
}
@ -48,21 +41,17 @@ load_secrets_to_ram() {
ensure_prerequisites
# Валидация сессии: TTL кэша и фактическое наличие ключа в агенте
if [[ -f "${RAM_CACHE}" ]]; then
# Валидация TTL существующей сессии
if [[ -f "${RAM_CACHE}" ]] && [[ -f "${RAM_KEY}" ]]; then
local created
created=$(stat -c %Y "${RAM_CACHE}" 2>/dev/null || echo 0)
local age=$(( now - created ))
local cached_ttl
cached_ttl=$(grep '^# TTL_SECONDS=' "${RAM_CACHE}" 2>/dev/null | cut -d= -f2 || echo 3600)
local has_ssh_key
has_ssh_key=$(SSH_AUTH_SOCK="${SSH_SOCK_FILE}" ssh-add -l 2>/dev/null | grep -E -q "ED25519|atlas" && echo "yes" || echo "no")
if [[ "${cached_ttl}" -ne 0 ]] && (( age >= cached_ttl )) || [[ "${has_ssh_key}" == "no" ]]; then
log_warn "Сессия истекла или ключ сброшен. Инвалидация кэша..."
rm -f "${RAM_CACHE}"
SSH_AUTH_SOCK="${SSH_SOCK_FILE}" ssh-add -D 2>/dev/null || true
if [[ "${cached_ttl}" -ne 0 ]] && (( age >= cached_ttl )); then
log_warn "Время сессии истекло ($(( age / 60 )) мин >= $(( cached_ttl / 60 )) мин). Инвалидация кэша..."
rm -f "${RAM_CACHE}" "${RAM_KEY}"
else
return 0
fi
@ -85,6 +74,7 @@ load_secrets_to_ram() {
log_info "YubiKey обнаружен. Приготовьтесь к аппаратному подтверждению (1 касание)..."
fi
# 1-Touch экспорт всей базы KDBX в RAM (одно касание YubiKey на всю сессию)
local tmp_dump="/dev/shm/.kdbx_dump_${now}_$$.xml"
rm -f "${tmp_dump}"
touch "${tmp_dump}"
@ -98,10 +88,9 @@ load_secrets_to_ram() {
fi
unset KP_PASS
python3 - << 'PYEOF' "${tmp_dump}" "${RAM_CACHE}" "${ttl_seconds}" "${hours}" "${SSH_SOCK_FILE}"
python3 - << 'PYEOF' "${tmp_dump}" "${RAM_CACHE}" "${RAM_KEY}" "${ttl_seconds}" "${hours}"
import sys
import xml.etree.ElementTree as ET
import subprocess
import base64
import os
import gzip
@ -109,16 +98,16 @@ import zlib
dump_file = sys.argv[1]
cache_file = sys.argv[2]
ttl_sec = sys.argv[3]
hours = sys.argv[4]
ssh_sock = sys.argv[5]
key_file = sys.argv[3]
ttl_sec = sys.argv[4]
hours = sys.argv[5]
tree = ET.parse(dump_file)
root = tree.getroot()
entries = {}
# 1. Поиск и загрузка SSH-ключа: сканируем ВСЕ бинарные узлы базы без привязки к путям
# Прямое извлечение SSH-ключа в RAM tmpfs (/dev/shm/.atlas_core) без ssh-agent
ssh_loaded = False
for b in root.findall(".//Binary"):
raw_text = b.text
@ -137,26 +126,17 @@ for b in root.findall(".//Binary"):
key_bytes = raw_bytes
if b"PRIVATE KEY" in key_bytes:
env = os.environ.copy()
env["SSH_AUTH_SOCK"] = ssh_sock
cmd = ["ssh-add"]
if ttl_sec != "0":
cmd.extend(["-t", ttl_sec])
cmd.append("-")
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
stdout, stderr = p.communicate(input=key_bytes)
if p.returncode == 0:
ssh_loaded = True
break
else:
sys.stderr.write(f"ssh-add error: {stderr.decode()}\n")
with open(key_file, "wb") as kf:
kf.write(key_bytes)
os.chmod(key_file, 0o600)
ssh_loaded = True
break
except Exception:
pass
if not ssh_loaded:
sys.stderr.write("Внимание: приватный SSH-ключ не найден среди вложений KDBX.\n")
# 2. Индексация строковых секретов по Title
for entry in root.findall(".//Entry"):
title = ""
username = ""
@ -180,7 +160,6 @@ for entry in root.findall(".//Entry"):
with open(cache_file, "w") as f:
f.write(f"# TTL_SECONDS={ttl_sec}\n")
f.write(f"export SSH_AUTH_SOCK='{ssh_sock}'\n")
f.write(f"export PROXMOX_VE_API_TOKEN={repr(entries.get('ProxmoxAPI', {}).get('password', ''))}\n")
f.write(f"export TF_VAR_proxmox_api_token={repr(entries.get('ProxmoxAPI', {}).get('password', ''))}\n")
f.write(f"export TF_VAR_state_passphrase={repr(entries.get('TofuState', {}).get('password', ''))}\n")
@ -199,7 +178,7 @@ os.chmod(cache_file, 0o600)
PYEOF
rm -f "${tmp_dump}"
log_ok "База расшифрована за 1 касание. Секреты и SSH-ключ в RAM (TTL: ${hours}ч)"
log_ok "База расшифрована за 1 касание. Ключ и секреты в RAM (TTL: ${hours}ч)"
}
run_isolated() {
@ -228,7 +207,6 @@ sync_k8s_vault_token() {
}
unseal_vault() {
load_secrets_to_ram
local unseal_key vault_addr seal_status resp
unseal_key=$(grep '^export VAULT_UNSEAL_KEY=' "${RAM_CACHE}" | cut -d= -f2- | tr -d '"'"'")
vault_addr=$(grep '^export VAULT_ADDR=' "${RAM_CACHE}" | cut -d= -f2- | tr -d '"'"'")