fix(ssh): direct ram key materialization without ssh-agent daemon
This commit is contained in:
parent
2f6f633aa1
commit
0e9dec3445
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 '"'"'")
|
||||
|
|
|
|||
Loading…
Reference in a new issue