From 44876c0bc2f1d7e74703ceb8d53a3d41949bc7d7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Nov 2025 20:11:09 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 6 ++- frontend/src/components/WebSshForm.vue | 23 ++++---- webssh-agent/Dockerfile | 21 ++++++-- webssh-agent/agent.js | 74 +++++++++++++------------- webssh-agent/docker-entrypoint.sh | 26 +++++++++ webssh-agent/utils/cleanupUtils.js | 58 ++++++++++---------- webssh-agent/utils/dockerUtils.js | 17 +++--- webssh-agent/utils/localUtils.js | 61 ++++++++++----------- webssh-agent/utils/userUtils.js | 20 +++---- 9 files changed, 173 insertions(+), 133 deletions(-) create mode 100644 webssh-agent/docker-entrypoint.sh diff --git a/docker-compose.yml b/docker-compose.yml index a4b1fa7..268bc06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -230,10 +230,12 @@ services: # WebSSH Agent для настройки VDS webssh-agent: - profiles: ["dev"] # Только для разработки build: context: ./webssh-agent dockerfile: Dockerfile + args: + WEBSSH_UID: ${LOCAL_UID:-1000} + WEBSSH_GID: ${LOCAL_GID:-1000} container_name: dapp-webssh-agent restart: unless-stopped dns: @@ -242,7 +244,7 @@ services: - 8.8.8.8 # Google (надежность, fallback) volumes: - ~/.ssh:/home/webssh/.ssh:rw - - /var/run/docker.sock:/var/run/docker.sock:ro # Только чтение для безопасности + - /var/run/docker.sock:/var/run/docker.sock:rw - /tmp:/tmp # для временных файлов - ./ssl:/app/ssl # для доступа к ключу шифрования security_opt: diff --git a/frontend/src/components/WebSshForm.vue b/frontend/src/components/WebSshForm.vue index dc20bb1..ed87b66 100644 --- a/frontend/src/components/WebSshForm.vue +++ b/frontend/src/components/WebSshForm.vue @@ -304,21 +304,25 @@ const validateForm = () => { addLog('error', 'Заполните все обязательные поля'); return false; } - // Валидация домена - const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - if (!domainRegex.test(form.domain)) { + const asciiDomain = encodeDomainForRequest(form.domain); + if (!asciiDomain) { + addLog('error', 'Введите корректный домен (например: example.com)'); + return false; + } + const domainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/; + if (!domainRegex.test(asciiDomain)) { addLog('error', 'Введите корректный домен (например: example.com)'); return false; } - // Валидация email + form.domain = asciiDomain; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(form.email)) { addLog('error', 'Введите корректный email адрес'); return false; } - - // Валидация логинов + if (form.ubuntuUser.length < 3 || form.ubuntuUser.length > 32) { addLog('error', 'Логин Ubuntu должен быть от 3 до 32 символов'); return false; @@ -327,7 +331,7 @@ const validateForm = () => { addLog('error', 'Логин Ubuntu должен начинаться с буквы и содержать только строчные буквы, цифры, _ и -'); return false; } - + if (form.dockerUser.length < 3 || form.dockerUser.length > 32) { addLog('error', 'Логин Docker должен быть от 3 до 32 символов'); return false; @@ -336,12 +340,7 @@ const validateForm = () => { addLog('error', 'Логин Docker должен начинаться с буквы и содержать только строчные буквы, цифры, _ и -'); return false; } - - // Валидация паролей убрана - доступ только через SSH ключи - - // Валидация ключа шифрования убрана - будет генерироваться автоматически - return true; }; const resetForm = () => { diff --git a/webssh-agent/Dockerfile b/webssh-agent/Dockerfile index 45c2f1a..727e5f0 100644 --- a/webssh-agent/Dockerfile +++ b/webssh-agent/Dockerfile @@ -14,13 +14,25 @@ RUN apt-get update && apt-get install -y \ gzip \ zip \ unzip \ + gosu \ && rm -rf /var/lib/apt/lists/* # Устанавливаем Docker CLI RUN curl -fsSL https://get.docker.com | sh # Создаем пользователя для безопасности -RUN useradd -m -s /bin/bash webssh +ARG WEBSSH_UID=1000 +ARG WEBSSH_GID=1000 +RUN if getent group ${WEBSSH_GID} >/dev/null; then \ + groupmod -n webssh "$(getent group ${WEBSSH_GID} | cut -d: -f1)"; \ + else \ + groupadd -g ${WEBSSH_GID} webssh; \ + fi && \ + if getent passwd ${WEBSSH_UID} >/dev/null; then \ + usermod -l webssh -d /home/webssh -m "$(getent passwd ${WEBSSH_UID} | cut -d: -f1)"; \ + else \ + useradd -m -u ${WEBSSH_UID} -g webssh -s /bin/bash webssh; \ + fi # Создаем рабочую директорию WORKDIR /app @@ -44,11 +56,10 @@ RUN mkdir -p /home/webssh/.ssh && \ # Добавляем пользователя в группу docker RUN usermod -aG docker webssh -# Переключаемся на пользователя -USER webssh +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh -# Открываем порт EXPOSE 3000 -# Команда запуска +ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["yarn", "start"] diff --git a/webssh-agent/agent.js b/webssh-agent/agent.js index 457ddaf..aab07e3 100644 --- a/webssh-agent/agent.js +++ b/webssh-agent/agent.js @@ -241,8 +241,8 @@ app.post('/vds/transfer-encryption-key', logRequest, async (req, res) => { // 4. Устанавливаем правильные права доступа к ключу на VDS log.info('🔒 Настройка прав доступа к ключу шифрования...'); - await execSshCommand(`sudo chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options); - await execSshCommand(`sudo chmod 600 /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options); + await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options); + await execSshCommand(`chmod 600 /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options); // 5. Проверяем, что ключ успешно передан const verifyResult = await execSshCommand(`ls -la /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options); @@ -344,13 +344,13 @@ app.post('/vds/setup', logRequest, async (req, res) => { sendWebSocketLog('info', '🐳 Установка Docker...', 'docker', 50); log.info('Установка Docker...'); await execSshCommand('curl -fsSL https://get.docker.com -o get-docker.sh', options); - await execSshCommand('sudo sh get-docker.sh', options); - await execSshCommand(`sudo usermod -aG docker ${dockerUser}`, options); + await execSshCommand('sh get-docker.sh', options); + await execSshCommand(`usermod -aG docker ${dockerUser}`, options); sendWebSocketLog('success', '✅ Docker установлен', 'docker', 55); // 6. Установка Docker Compose - await execSshCommand('sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose', options); - await execSshCommand('sudo chmod +x /usr/local/bin/docker-compose', options); + await execSshCommand('curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose', options); + await execSshCommand('chmod +x /usr/local/bin/docker-compose', options); // 7. Отключение парольной аутентификации await disablePasswordAuth(options); @@ -360,7 +360,7 @@ app.post('/vds/setup', logRequest, async (req, res) => { // 8.1. Установка fail2ban для защиты от SSH атак log.info('Установка fail2ban для защиты от SSH атак...'); - await execSshCommand('sudo apt-get install -y fail2ban', options); + await execSshCommand('apt-get install -y fail2ban', options); // Создание конфигурации fail2ban для SSH с увеличенными лимитами const fail2banConfig = `[sshd] @@ -379,15 +379,15 @@ findtime = 3600 maxretry = 3 bantime = 3600`; - await execSshCommand(`echo '${fail2banConfig}' | sudo tee /etc/fail2ban/jail.local`, options); - await execSshCommand('sudo systemctl enable fail2ban', options); - await execSshCommand('sudo systemctl start fail2ban', options); + await execSshCommand(`echo '${fail2banConfig}' | tee /etc/fail2ban/jail.local`, options); + await execSshCommand('systemctl enable fail2ban', options); + await execSshCommand('systemctl start fail2ban', options); log.success('fail2ban настроен для защиты от SSH атак'); // 9. Создание директории для ключей шифрования - await execSshCommand(`sudo mkdir -p /home/${dockerUser}/dapp/ssl/keys`, options); - await execSshCommand(`sudo chmod 700 /home/${dockerUser}/dapp/ssl/keys`, options); - await execSshCommand(`sudo chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys`, options); + await execSshCommand(`mkdir -p /home/${dockerUser}/dapp/ssl/keys`, options); + await execSshCommand(`chmod 700 /home/${dockerUser}/dapp/ssl/keys`, options); + await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys`, options); log.success('Директория для ключа шифрования подготовлена'); // 10. Проверка и удаление системного nginx для избежания конфликтов портов @@ -398,12 +398,12 @@ findtime = 3600 log.info('⚠️ Обнаружен системный nginx - удаляем для освобождения портов 80/443...'); // Остановка и удаление системного nginx - await execSshCommand('sudo systemctl stop nginx || true', options); - await execSshCommand('sudo systemctl disable nginx || true', options); - await execSshCommand('sudo systemctl mask nginx || true', options); - await execSshCommand('sudo pkill -f nginx || true', options); - await execSshCommand('sudo apt-get purge -y nginx nginx-common nginx-full || true', options); - await execSshCommand('sudo apt-get autoremove -y || true', options); + await execSshCommand('systemctl stop nginx || true', options); + await execSshCommand('systemctl disable nginx || true', options); + await execSshCommand('systemctl mask nginx || true', options); + await execSshCommand('pkill -f nginx || true', options); + await execSshCommand('apt-get purge -y nginx nginx-common nginx-full || true', options); + await execSshCommand('apt-get autoremove -y || true', options); log.success('✅ Системный nginx полностью удален, порты 80/443 освобождены для Docker nginx'); } else { @@ -412,11 +412,11 @@ findtime = 3600 // 11. Создание временных SSL сертификатов для запуска frontend-nginx log.info('🔒 Создание временных SSL сертификатов...'); - await execSshCommand(`sudo mkdir -p /etc/letsencrypt/live/${domain}`, options); - await execSshCommand(`sudo mkdir -p /var/www/certbot`, options); + await execSshCommand(`mkdir -p /etc/letsencrypt/live/${domain}`, options); + await execSshCommand(`mkdir -p /var/www/certbot`, options); // Создаем временный самоподписанный сертификат - const tempCertCommand = `sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/letsencrypt/live/${domain}/privkey.pem -out /etc/letsencrypt/live/${domain}/fullchain.pem -subj '/C=US/ST=State/L=City/O=Organization/CN=${domain}'`; + const tempCertCommand = `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/letsencrypt/live/${domain}/privkey.pem -out /etc/letsencrypt/live/${domain}/fullchain.pem -subj '/C=US/ST=State/L=City/O=Organization/CN=${domain}'`; await execSshCommand(tempCertCommand, options); log.success('Временный SSL сертификат создан'); @@ -475,21 +475,21 @@ WS_BACKEND_CONTAINER=dapp-backend`; // 16. Запуск приложения log.info('Запуск приложения...'); - await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml up -d`, options); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml up -d`, options); // 16.1. 🆕 Настройка CORS заголовков в nginx для API log.info('🔧 Настройка CORS заголовков в nginx для API...'); - await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec frontend-nginx sed -i '/add_header X-XSS-Protection/a\\ add_header Access-Control-Allow-Origin \"https://${domain}\" always;\\ add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;\\ add_header Access-Control-Allow-Headers \"Content-Type, Authorization, X-Requested-With\" always;\\ add_header Access-Control-Allow-Credentials \"true\" always;' /etc/nginx/nginx.conf`, options); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec frontend-nginx sed -i '/add_header X-XSS-Protection/a\\ add_header Access-Control-Allow-Origin \"https://${domain}\" always;\\ add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;\\ add_header Access-Control-Allow-Headers \"Content-Type, Authorization, X-Requested-With\" always;\\ add_header Access-Control-Allow-Credentials \"true\" always;' /etc/nginx/nginx.conf`, options); // Перезапускаем nginx с новой конфигурацией - await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml restart frontend-nginx`, options); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx`, options); log.success('✅ CORS заголовки настроены в nginx для API'); // 16.0. 🆕 Получение реального SSL сертификата через Let's Encrypt (опционально) log.info('Получение реального SSL сертификата через Let\'s Encrypt...'); // Получаем SSL сертификат через certbot - const certbotResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml run --rm certbot`, options); + const certbotResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot`, options); if (certbotResult.code === 0) { log.success('Реальный SSL сертификат успешно получен'); @@ -512,9 +512,9 @@ else echo "$(date): Ошибка обновления SSL сертификатов" >> /var/log/ssl-renewal.log fi `; - await execSshCommand(`echo '${renewScript}' | sudo tee /home/${dockerUser}/dapp/renew-ssl.sh`, options); - await execSshCommand(`sudo chmod +x /home/${dockerUser}/dapp/renew-ssl.sh`, options); - await execSshCommand(`echo "0 12 * * * /home/${dockerUser}/dapp/renew-ssl.sh" | sudo crontab -`, options); + await execSshCommand(`echo '${renewScript}' | tee /home/${dockerUser}/dapp/renew-ssl.sh`, options); + await execSshCommand(`chmod +x /home/${dockerUser}/dapp/renew-ssl.sh`, options); + await execSshCommand(`echo "0 12 * * * /home/${dockerUser}/dapp/renew-ssl.sh" | crontab -`, options); log.success('Автоматическое обновление SSL сертификатов через Docker настроено (ежедневно в 12:00)'); // 16.1. 🆕 Ожидание готовности базы данных с повторными попытками @@ -524,7 +524,7 @@ fi const maxAttempts = 30; while (!dbReady && attempts < maxAttempts) { - const dbCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T postgres pg_isready -U dapp_user -d dapp_db`, options); + const dbCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T postgres pg_isready -U dapp_user -d dapp_db`, options); if (dbCheckResult.code === 0) { dbReady = true; log.success('База данных готова к работе'); @@ -541,13 +541,13 @@ fi // 16.2. 🆕 Проверка целостности переданной базы данных log.info('Проверка целостности переданной базы данных...'); - const tableCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T postgres psql -U dapp_user -d dapp_db -c "\\dt"`, options); + const tableCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T postgres psql -U dapp_user -d dapp_db -c "\\dt"`, options); if (tableCheckResult.code === 0 && tableCheckResult.stdout.includes('email_settings')) { log.success('База данных содержит все необходимые таблицы (email_settings найдена)'); // Дополнительная проверка количества таблиц - const tableCountResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T postgres psql -U dapp_user -d dapp_db -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"`, options); + const tableCountResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T postgres psql -U dapp_user -d dapp_db -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"`, options); if (tableCountResult.code === 0) { log.info(`Количество таблиц в базе данных: ${tableCountResult.stdout.trim()}`); } @@ -558,13 +558,13 @@ fi // 16.3. 🆕 Улучшенная проверка ключа шифрования в контейнере backend log.info('Проверка ключа шифрования в backend контейнере...'); - const keyCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T backend ls -la /app/ssl/keys/`, options); + const keyCheckResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T backend ls -la /app/ssl/keys/`, options); if (keyCheckResult.code === 0 && keyCheckResult.stdout.includes('full_db_encryption.key')) { log.success('Ключ шифрования найден в backend контейнере'); // Дополнительная проверка содержимого ключа - const keyContentResult = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T backend head -c 50 /app/ssl/keys/full_db_encryption.key`, options); + const keyContentResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T backend head -c 50 /app/ssl/keys/full_db_encryption.key`, options); if (keyContentResult.code === 0) { log.info('Ключ шифрования доступен для чтения в контейнере'); } @@ -574,10 +574,10 @@ fi log.info('Попытка повторного монтирования ключа...'); // Перезапуск backend контейнера с правильным монтированием - await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml restart backend`, options); + await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart backend`, options); await new Promise(resolve => setTimeout(resolve, 5000)); - const retryKeyCheck = await execSshCommand(`cd /home/${dockerUser}/dapp && sudo docker compose -f docker-compose.prod.yml exec -T backend ls -la /app/ssl/keys/`, options); + const retryKeyCheck = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec -T backend ls -la /app/ssl/keys/`, options); if (retryKeyCheck.code === 0 && retryKeyCheck.stdout.includes('full_db_encryption.key')) { log.success('Ключ шифрования найден после перезапуска backend контейнера'); } else { @@ -587,7 +587,7 @@ fi // 17. Проверка статуса контейнеров log.info('Проверка статуса контейнеров...'); - const containersResult = await execSshCommand('sudo docker ps --format "table {{.Names}}\\t{{.Status}}"', options); + const containersResult = await execSshCommand('docker ps --format "table {{.Names}}\\t{{.Status}}"', options); log.info('Статус контейнеров:\\n' + containersResult.stdout); log.success('VDS настроена и приложение запущено'); diff --git a/webssh-agent/docker-entrypoint.sh b/webssh-agent/docker-entrypoint.sh new file mode 100644 index 0000000..b8111b8 --- /dev/null +++ b/webssh-agent/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +SOCK_PATH=${DOCKER_SOCK_PATH:-/var/run/docker.sock} + +if [ -S "${SOCK_PATH}" ]; then + SOCK_GID=$(stat -c %g "${SOCK_PATH}" || echo "") + if [ -n "${SOCK_GID}" ]; then + GROUP_LOOKUP=$(getent group "${SOCK_GID}" || true) + if [ -n "${GROUP_LOOKUP}" ]; then + GROUP_NAME=$(echo "${GROUP_LOOKUP}" | cut -d: -f1) + else + GROUP_NAME=docker-host + if ! getent group "${GROUP_NAME}" >/dev/null 2>&1; then + groupadd -g "${SOCK_GID}" "${GROUP_NAME}" || true + fi + fi + + usermod -aG "${GROUP_NAME}" webssh || true + fi +fi + +chown -R webssh:webssh /home/webssh/.ssh 2>/dev/null || true + +exec gosu webssh "$@" + diff --git a/webssh-agent/utils/cleanupUtils.js b/webssh-agent/utils/cleanupUtils.js index e49ecaf..9ecdfe8 100644 --- a/webssh-agent/utils/cleanupUtils.js +++ b/webssh-agent/utils/cleanupUtils.js @@ -9,14 +9,14 @@ const cleanupVdsServer = async (options) => { // Остановка и удаление существующих Docker контейнеров log.info('Остановка существующих Docker контейнеров...'); - await execSshCommand('sudo docker stop $(sudo docker ps -aq) 2>/dev/null || true', options); - await execSshCommand('sudo docker rm $(sudo docker ps -aq) 2>/dev/null || true', options); + await execSshCommand('docker ps -aq | xargs -r docker stop 2>/dev/null || true', options); + await execSshCommand('docker ps -aq | xargs -r docker rm 2>/dev/null || true', options); // Удаление Docker образов и очистка системы log.info('Очистка Docker образов и системы...'); - await execSshCommand('sudo docker system prune -af || true', options); - await execSshCommand('sudo docker volume prune -f || true', options); - await execSshCommand('sudo docker network prune -f || true', options); + await execSshCommand('docker system prune -af || true', options); + await execSshCommand('docker volume prune -f || true', options); + await execSshCommand('docker network prune -f || true', options); // 🆕 Умная проверка и удаление системного nginx для избежания конфликтов портов log.info('🔍 Проверка наличия системного nginx...'); @@ -26,12 +26,12 @@ const cleanupVdsServer = async (options) => { log.info('⚠️ Обнаружен системный nginx - удаляем для освобождения портов 80/443...'); // Полная остановка и удаление системного nginx - await execSshCommand('sudo systemctl stop nginx || true', options); - await execSshCommand('sudo systemctl disable nginx || true', options); - await execSshCommand('sudo systemctl mask nginx || true', options); - await execSshCommand('sudo pkill -f nginx || true', options); - await execSshCommand('sudo apt-get purge -y nginx nginx-common nginx-full || true', options); - await execSshCommand('sudo apt-get autoremove -y || true', options); + await execSshCommand('systemctl stop nginx || true', options); + await execSshCommand('systemctl disable nginx || true', options); + await execSshCommand('systemctl mask nginx || true', options); + await execSshCommand('pkill -f nginx || true', options); + await execSshCommand('apt-get purge -y nginx nginx-common nginx-full || true', options); + await execSshCommand('apt-get autoremove -y || true', options); log.success('✅ Системный nginx полностью удален, порты 80/443 освобождены для Docker nginx'); } else { @@ -40,18 +40,18 @@ const cleanupVdsServer = async (options) => { // Остановка других конфликтующих сервисов log.info('Остановка других конфликтующих сервисов...'); - await execSshCommand('sudo systemctl stop apache2 2>/dev/null || true', options); - await execSshCommand('sudo systemctl disable apache2 2>/dev/null || true', options); - await execSshCommand('sudo systemctl mask apache2 2>/dev/null || true', options); + await execSshCommand('systemctl stop apache2 2>/dev/null || true', options); + await execSshCommand('systemctl disable apache2 2>/dev/null || true', options); + await execSshCommand('systemctl mask apache2 2>/dev/null || true', options); // Очистка старых пакетов log.info('Очистка старых пакетов...'); - await execSshCommand('sudo apt-get autoremove -y || true', options); - await execSshCommand('sudo apt-get autoclean || true', options); + await execSshCommand('apt-get autoremove -y || true', options); + await execSshCommand('apt-get autoclean || true', options); // Очистка временных файлов log.info('Очистка временных файлов...'); - await execSshCommand('sudo rm -rf /tmp/* /var/tmp/* 2>/dev/null || true', options); + await execSshCommand('rm -rf /tmp/* /var/tmp/* 2>/dev/null || true', options); log.success('VDS сервер очищен'); }; @@ -63,13 +63,13 @@ const setupRootSshKeys = async (publicKey, options) => { log.info('Настройка SSH ключей...'); // Создание директории .ssh для root - await execSshCommand('sudo mkdir -p /root/.ssh', options); - await execSshCommand('sudo chmod 700 /root/.ssh', options); + await execSshCommand('mkdir -p /root/.ssh', options); + await execSshCommand('chmod 700 /root/.ssh', options); // Добавление публичного ключа в authorized_keys - await execSshCommand(`echo "${publicKey}" | sudo tee -a /root/.ssh/authorized_keys`, options); - await execSshCommand('sudo chmod 600 /root/.ssh/authorized_keys', options); - await execSshCommand('sudo chown root:root /root/.ssh/authorized_keys', options); + await execSshCommand(`echo "${publicKey}" >> /root/.ssh/authorized_keys`, options); + await execSshCommand('chmod 600 /root/.ssh/authorized_keys', options); + await execSshCommand('chown root:root /root/.ssh/authorized_keys', options); log.success('SSH ключи созданы и публичный ключ добавлен в authorized_keys'); }; @@ -79,9 +79,9 @@ const setupRootSshKeys = async (publicKey, options) => { */ const disablePasswordAuth = async (options) => { log.info('Отключение парольной аутентификации...'); - await execSshCommand('sudo sed -i "s/#PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options); - await execSshCommand('sudo sed -i "s/PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options); - await execSshCommand('sudo systemctl restart ssh', options); + await execSshCommand('sed -i "s/#PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options); + await execSshCommand('sed -i "s/PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config', options); + await execSshCommand('systemctl restart ssh', options); log.success('Парольная аутентификация отключена, доступ только через SSH ключи'); }; @@ -90,10 +90,10 @@ const disablePasswordAuth = async (options) => { */ const setupFirewall = async (options) => { log.info('Настройка firewall...'); - await execSshCommand('sudo ufw --force enable', options); - await execSshCommand('sudo ufw allow ssh', options); - await execSshCommand('sudo ufw allow 80', options); - await execSshCommand('sudo ufw allow 443', options); + await execSshCommand('ufw --force enable', options); + await execSshCommand('ufw allow ssh', options); + await execSshCommand('ufw allow 80', options); + await execSshCommand('ufw allow 443', options); log.success('Firewall настроен'); }; diff --git a/webssh-agent/utils/dockerUtils.js b/webssh-agent/utils/dockerUtils.js index cc59258..7df991e 100644 --- a/webssh-agent/utils/dockerUtils.js +++ b/webssh-agent/utils/dockerUtils.js @@ -29,6 +29,10 @@ const execDockerCommand = async (command) => { return execAsync(command); }; +const execLocalCommand = async (command, options = {}) => { + return execAsync(command, { maxBuffer: options.maxBuffer || 1024 * 1024 * 50 }); +}; + /** * Экспорт Docker образов и данных с локальной машины */ @@ -89,9 +93,8 @@ const exportDockerImages = async (sendWebSocketLog) => { const tarFiles = images.map(img => img.file).join(' '); const dataFiles = 'postgres_data.tar.gz ollama_data.tar.gz vector_search_data.tar.gz'; - // Безопасное создание архива через CLI const archiveCommand = `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles}`; - await execDockerCommand(archiveCommand); + await execLocalCommand(archiveCommand); sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80); } catch (error) { @@ -110,7 +113,7 @@ const exportVolumeData = async (volumeName, outputFile, sendWebSocketLog, progre try { // Безопасный экспорт через CLI с временным контейнером const exportCommand = `docker run --rm -v ${volumeName}:/data:ro -v /tmp:/backup alpine tar czf /backup/${outputFile} -C /data .`; - await execDockerCommand(exportCommand); + await execLocalCommand(exportCommand); sendWebSocketLog('success', `✅ Экспорт ${outputFile} завершен`, 'export_data', progress); } catch (error) { @@ -216,8 +219,8 @@ docker images | grep -E "digital_legal_entitydle|postgres" echo "📋 Доступные volumes:" docker volume ls | grep dapp_`; - await execSshCommand(`echo '${importScript}' | sudo tee /home/${dockerUser}/dapp/import-images-and-data.sh`, options); - await execSshCommand(`sudo chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options); + await execSshCommand(`echo '${importScript}' | tee /home/${dockerUser}/dapp/import-images-and-data.sh`, options); + await execSshCommand(`chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options); // Импортируем образы и данные log.info('Импорт Docker образов и данных...'); @@ -238,9 +241,7 @@ docker volume ls | grep dapp_`; const cleanupLocalFiles = async () => { log.info('Очистка временных файлов на хосте...'); try { - await fs.remove('/tmp/dapp-*.tar'); - await fs.remove('/tmp/*_data.tar.gz'); - await fs.remove('/tmp/docker-images-and-data.tar.gz'); + await execLocalCommand("rm -f /tmp/dapp-*.tar /tmp/*_data.tar.gz /tmp/docker-images-and-data.tar.gz"); log.success('Временные файлы очищены'); } catch (error) { log.error('Ошибка очистки файлов: ' + error.message); diff --git a/webssh-agent/utils/localUtils.js b/webssh-agent/utils/localUtils.js index c96693f..9b2c666 100644 --- a/webssh-agent/utils/localUtils.js +++ b/webssh-agent/utils/localUtils.js @@ -1,4 +1,5 @@ const { exec } = require('child_process'); +const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const log = require('./logger'); @@ -14,10 +15,10 @@ const sshConfigPath = path.join(sshDir, 'config'); const execLocalCommand = async (command) => { return new Promise((resolve) => { exec(command, (error, stdout, stderr) => { - resolve({ - code: error ? error.code : 0, - stdout: stdout || '', - stderr: stderr || '' + resolve({ + code: error ? error.code : 0, + stdout: stdout || '', + stderr: stderr || '' }); }); }); @@ -28,35 +29,35 @@ const execLocalCommand = async (command) => { */ const createSshKeys = async (email) => { log.info('Создание SSH ключей на хосте...'); - - return new Promise((resolve) => { - // Сначала исправляем права доступа к SSH конфигу - exec(`mkdir -p "${sshDir}" && chmod 700 "${sshDir}" && chmod 600 "${sshConfigPath}" 2>/dev/null || true`, (configError) => { - if (configError) { - log.warn('Не удалось исправить права доступа к SSH конфигу: ' + configError.message); + + await fs.ensureDir(sshDir); + await execLocalCommand(`chmod 700 "${sshDir}" && chmod 600 "${sshConfigPath}" 2>/dev/null || true`); + + const privateExists = await fs.pathExists(privateKeyPath); + const publicExists = await fs.pathExists(publicKeyPath); + + if (privateExists && publicExists) { + log.info('SSH ключи уже существуют – используем текущую пару'); + await execLocalCommand(`chmod 600 "${privateKeyPath}" && chmod 644 "${publicKeyPath}"`); + return; + } + + await new Promise((resolve) => { + exec(`ssh-keygen -q -t rsa -b 4096 -C "${email}" -f "${privateKeyPath}" -N ""`, (error) => { + if (error) { + log.error('Ошибка создания SSH ключей: ' + error.message); + return resolve(); } - - // Создаем SSH ключи - exec(`ssh-keygen -t rsa -b 4096 -C "${email}" -f "${privateKeyPath}" -N ""`, (error, stdout, stderr) => { - if (error) { - log.error('Ошибка создания SSH ключей: ' + error.message); + + log.success('SSH ключи успешно созданы на хосте'); + + exec(`chmod 600 "${privateKeyPath}" && chmod 644 "${publicKeyPath}"`, (permError) => { + if (permError) { + log.warn('Не удалось установить права доступа к SSH ключам: ' + permError.message); } else { - log.success('SSH ключи успешно созданы на хосте'); - - // Устанавливаем правильные права доступа к созданным ключам - exec(`chmod 600 "${privateKeyPath}" && chmod 644 "${publicKeyPath}"`, (permError) => { - if (permError) { - log.warn('Не удалось установить права доступа к SSH ключам: ' + permError.message); - } else { - log.success('Права доступа к SSH ключам установлены'); - } - resolve(); - }); - } - - if (error) { - resolve(); + log.success('Права доступа к SSH ключам установлены'); } + resolve(); }); }); }); diff --git a/webssh-agent/utils/userUtils.js b/webssh-agent/utils/userUtils.js index d7bcdb8..3bdcf39 100644 --- a/webssh-agent/utils/userUtils.js +++ b/webssh-agent/utils/userUtils.js @@ -8,15 +8,15 @@ const createUserWithSshKeys = async (username, publicKey, options) => { log.info(`Создание пользователя ${username}...`); // Создание пользователя - await execSshCommand(`sudo useradd -m -s /bin/bash ${username} || true`, options); - await execSshCommand(`sudo usermod -aG sudo ${username}`, options); + await execSshCommand(`useradd -m -s /bin/bash ${username} || true`, options); + await execSshCommand(`usermod -aG sudo ${username}`, options); // Настройка SSH ключей для пользователя - await execSshCommand(`sudo mkdir -p /home/${username}/.ssh`, options); - await execSshCommand(`echo "${publicKey}" | sudo tee /home/${username}/.ssh/authorized_keys`, options); - await execSshCommand(`sudo chown -R ${username}:${username} /home/${username}/.ssh`, options); - await execSshCommand(`sudo chmod 700 /home/${username}/.ssh`, options); - await execSshCommand(`sudo chmod 600 /home/${username}/.ssh/authorized_keys`, options); + await execSshCommand(`mkdir -p /home/${username}/.ssh`, options); + await execSshCommand(`echo "${publicKey}" > /home/${username}/.ssh/authorized_keys`, options); + await execSshCommand(`chown -R ${username}:${username} /home/${username}/.ssh`, options); + await execSshCommand(`chmod 700 /home/${username}/.ssh`, options); + await execSshCommand(`chmod 600 /home/${username}/.ssh/authorized_keys`, options); log.success(`Пользователь ${username} создан с SSH ключами`); }; @@ -32,11 +32,11 @@ const createAllUsers = async (ubuntuUser, dockerUser, publicKey, options) => { await createUserWithSshKeys(dockerUser, publicKey, options); // Добавление пользователя Docker в группу docker - await execSshCommand(`sudo usermod -aG docker ${dockerUser}`, options); + await execSshCommand(`usermod -aG docker ${dockerUser}`, options); // Создание директории для приложения - await execSshCommand(`sudo mkdir -p /home/${dockerUser}/dapp`, options); - await execSshCommand(`sudo chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp`, options); + await execSshCommand(`mkdir -p /home/${dockerUser}/dapp`, options); + await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp`, options); log.success('Все пользователи созданы'); };