feat: новая функция
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
@@ -337,11 +341,6 @@ const validateForm = () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Валидация паролей убрана - доступ только через SSH ключи
|
||||
|
||||
|
||||
// Валидация ключа шифрования убрана - будет генерироваться автоматически
|
||||
|
||||
return true;
|
||||
};
|
||||
const resetForm = () => {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 настроена и приложение запущено');
|
||||
|
||||
26
webssh-agent/docker-entrypoint.sh
Normal file
26
webssh-agent/docker-entrypoint.sh
Normal file
@@ -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 "$@"
|
||||
|
||||
@@ -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 настроен');
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
@@ -29,34 +30,34 @@ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('Все пользователи созданы');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user