557 lines
18 KiB
PowerShell
557 lines
18 KiB
PowerShell
# ============================================================================
|
||
# Скрипт для создания и обновления SSL сертификата Let's Encrypt
|
||
# с использованием DNS-валидации через API reg.ru (PowerShell версия)
|
||
#
|
||
# Автор: GitHub Copilot
|
||
# Дата: 27.10.2025
|
||
# Описание: PowerShell версия скрипта для Windows окружения
|
||
# ============================================================================
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
Автоматизация получения SSL сертификатов Let's Encrypt через DNS-валидацию reg.ru
|
||
|
||
.DESCRIPTION
|
||
Скрипт позволяет автоматически получать и обновлять SSL сертификаты Let's Encrypt
|
||
для доменов на reg.ru используя DNS-01 challenge через API
|
||
|
||
.PARAMETER Domain
|
||
Основной домен (например, dfv24.com)
|
||
|
||
.PARAMETER Email
|
||
Email для уведомлений Let's Encrypt
|
||
|
||
.PARAMETER RegRuUsername
|
||
Имя пользователя reg.ru
|
||
|
||
.PARAMETER RegRuPassword
|
||
Пароль reg.ru
|
||
|
||
.PARAMETER Wildcard
|
||
Создать wildcard сертификат (*.domain.com)
|
||
|
||
.PARAMETER ConfigFile
|
||
Путь к файлу конфигурации JSON
|
||
|
||
.EXAMPLE
|
||
.\letsencrypt_regru.ps1 -Domain "dfv24.com" -Email "admin@dfv24.com" -Wildcard
|
||
#>
|
||
|
||
param(
|
||
[Parameter(Mandatory=$false)]
|
||
[string]$Domain = "dfv24.com",
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[string]$Email = "admin@dfv24.com",
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[string]$RegRuUsername = "",
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[string]$RegRuPassword = "",
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[switch]$Wildcard = $true,
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[string]$ConfigFile = ".\config.json",
|
||
|
||
[Parameter(Mandatory=$false)]
|
||
[switch]$Verbose = $false
|
||
)
|
||
|
||
# ============================================================================
|
||
# КОНФИГУРАЦИЯ
|
||
# ============================================================================
|
||
|
||
$Script:Config = @{
|
||
RegRuApiUrl = "https://api.reg.ru/api/regru2"
|
||
CertbotPath = "certbot"
|
||
LogFile = ".\letsencrypt_regru.log"
|
||
DnsPropagationWait = 60
|
||
DnsCheckAttempts = 10
|
||
DnsCheckInterval = 10
|
||
}
|
||
|
||
# ============================================================================
|
||
# ФУНКЦИИ ЛОГИРОВАНИЯ
|
||
# ============================================================================
|
||
|
||
function Write-Log {
|
||
param(
|
||
[string]$Message,
|
||
[string]$Level = "INFO"
|
||
)
|
||
|
||
$Timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
|
||
$LogMessage = "[$Timestamp] [$Level] $Message"
|
||
|
||
# Вывод в консоль
|
||
switch ($Level) {
|
||
"ERROR" { Write-Host $LogMessage -ForegroundColor Red }
|
||
"WARNING" { Write-Host $LogMessage -ForegroundColor Yellow }
|
||
"SUCCESS" { Write-Host $LogMessage -ForegroundColor Green }
|
||
default { Write-Host $LogMessage }
|
||
}
|
||
|
||
# Запись в файл
|
||
Add-Content -Path $Script:Config.LogFile -Value $LogMessage
|
||
}
|
||
|
||
# ============================================================================
|
||
# ФУНКЦИИ РАБОТЫ С КОНФИГУРАЦИЕЙ
|
||
# ============================================================================
|
||
|
||
function Load-Configuration {
|
||
param([string]$ConfigPath)
|
||
|
||
Write-Log "Загрузка конфигурации из: $ConfigPath"
|
||
|
||
if (Test-Path $ConfigPath) {
|
||
try {
|
||
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
|
||
|
||
# Обновляем параметры скрипта из конфигурации
|
||
if ($config.domain) { $Script:Domain = $config.domain }
|
||
if ($config.email) { $Script:Email = $config.email }
|
||
if ($config.regru_username) { $Script:RegRuUsername = $config.regru_username }
|
||
if ($config.regru_password) { $Script:RegRuPassword = $config.regru_password }
|
||
if ($null -ne $config.wildcard) { $Script:Wildcard = $config.wildcard }
|
||
|
||
Write-Log "Конфигурация успешно загружена" "SUCCESS"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка при загрузке конфигурации: $_" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
else {
|
||
Write-Log "Файл конфигурации не найден: $ConfigPath" "WARNING"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Create-SampleConfig {
|
||
param([string]$OutputPath = ".\config.json")
|
||
|
||
$sampleConfig = @{
|
||
regru_username = "your_username"
|
||
regru_password = "your_password"
|
||
domain = "dfv24.com"
|
||
wildcard = $true
|
||
email = "admin@dfv24.com"
|
||
dns_propagation_wait = 60
|
||
dns_check_attempts = 10
|
||
dns_check_interval = 10
|
||
}
|
||
|
||
$sampleConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath
|
||
Write-Log "Пример конфигурации создан: $OutputPath" "SUCCESS"
|
||
}
|
||
|
||
# ============================================================================
|
||
# ФУНКЦИИ РАБОТЫ С REG.RU API
|
||
# ============================================================================
|
||
|
||
function Invoke-RegRuApi {
|
||
param(
|
||
[string]$Method,
|
||
[hashtable]$Params
|
||
)
|
||
|
||
$url = "$($Script:Config.RegRuApiUrl)/$Method"
|
||
|
||
# Добавляем учетные данные
|
||
$Params["username"] = $Script:RegRuUsername
|
||
$Params["password"] = $Script:RegRuPassword
|
||
$Params["output_format"] = "json"
|
||
|
||
Write-Log "Отправка запроса к API: $Method" "DEBUG"
|
||
|
||
try {
|
||
$response = Invoke-RestMethod -Uri $url -Method Post -Body $Params -ContentType "application/x-www-form-urlencoded"
|
||
|
||
if ($response.result -eq "success") {
|
||
Write-Log "Запрос $Method выполнен успешно" "DEBUG"
|
||
return $response
|
||
}
|
||
else {
|
||
$errorMsg = if ($response.error_text) { $response.error_text } else { "Неизвестная ошибка" }
|
||
Write-Log "Ошибка API: $errorMsg" "ERROR"
|
||
throw "API Error: $errorMsg"
|
||
}
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка HTTP запроса: $_" "ERROR"
|
||
throw
|
||
}
|
||
}
|
||
|
||
function Get-DnsRecords {
|
||
param([string]$Domain)
|
||
|
||
Write-Log "Получение DNS записей для домена: $Domain"
|
||
|
||
$params = @{
|
||
domain = $Domain
|
||
}
|
||
|
||
$response = Invoke-RegRuApi -Method "zone/get_resource_records" -Params $params
|
||
|
||
if ($response.answer -and $response.answer.records) {
|
||
$records = $response.answer.records
|
||
Write-Log "Получено $($records.Count) DNS записей"
|
||
return $records
|
||
}
|
||
else {
|
||
Write-Log "DNS записи не найдены" "WARNING"
|
||
return @()
|
||
}
|
||
}
|
||
|
||
function Add-TxtRecord {
|
||
param(
|
||
[string]$Domain,
|
||
[string]$Subdomain,
|
||
[string]$TxtValue
|
||
)
|
||
|
||
Write-Log "Добавление TXT записи: $Subdomain.$Domain = $TxtValue"
|
||
|
||
$params = @{
|
||
domain = $Domain
|
||
subdomain = $Subdomain
|
||
text = $TxtValue
|
||
output_content_type = "plain"
|
||
}
|
||
|
||
try {
|
||
$null = Invoke-RegRuApi -Method "zone/add_txt" -Params $params
|
||
Write-Log "TXT запись успешно добавлена" "SUCCESS"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Не удалось добавить TXT запись: $_" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Remove-TxtRecord {
|
||
param(
|
||
[string]$Domain,
|
||
[string]$Subdomain,
|
||
[string]$TxtValue
|
||
)
|
||
|
||
Write-Log "Удаление TXT записи: $Subdomain.$Domain"
|
||
|
||
# Получаем список записей
|
||
$records = Get-DnsRecords -Domain $Domain
|
||
|
||
# Ищем нужную TXT запись
|
||
$record = $records | Where-Object {
|
||
$_.rectype -eq "TXT" -and
|
||
$_.subdomain -eq $Subdomain -and
|
||
$_.text -eq $TxtValue
|
||
} | Select-Object -First 1
|
||
|
||
if (-not $record) {
|
||
Write-Log "TXT запись для удаления не найдена" "WARNING"
|
||
return $false
|
||
}
|
||
|
||
$params = @{
|
||
domain = $Domain
|
||
record_id = $record.id
|
||
}
|
||
|
||
try {
|
||
$null = Invoke-RegRuApi -Method "zone/remove_record" -Params $params
|
||
Write-Log "TXT запись успешно удалена" "SUCCESS"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Не удалось удалить TXT запись: $_" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
# ============================================================================
|
||
# ФУНКЦИИ РАБОТЫ С CERTBOT
|
||
# ============================================================================
|
||
|
||
function Test-CertbotInstalled {
|
||
try {
|
||
$version = & certbot --version 2>&1
|
||
Write-Log "Certbot установлен: $version"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Certbot не установлен!" "ERROR"
|
||
Write-Log "Установите Certbot: https://certbot.eff.org/instructions" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Get-CertificateExpiry {
|
||
param([string]$Domain)
|
||
|
||
$certPath = Join-Path $env:ProgramData "letsencrypt\live\$Domain\cert.pem"
|
||
|
||
if (-not (Test-Path $certPath)) {
|
||
Write-Log "Сертификат не найден: $certPath"
|
||
return $null
|
||
}
|
||
|
||
try {
|
||
# Используем openssl для проверки сертификата
|
||
$expiryText = & openssl x509 -enddate -noout -in $certPath 2>&1
|
||
|
||
if ($expiryText -match "notAfter=(.+)") {
|
||
$expiryDate = [DateTime]::Parse($matches[1])
|
||
$daysLeft = ($expiryDate - (Get-Date)).Days
|
||
|
||
Write-Log "Сертификат истекает: $($expiryDate.ToString('yyyy-MM-dd'))"
|
||
Write-Log "Осталось дней: $daysLeft"
|
||
|
||
return $daysLeft
|
||
}
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка при проверке сертификата: $_" "ERROR"
|
||
}
|
||
|
||
return $null
|
||
}
|
||
|
||
function Invoke-DnsChallenge {
|
||
param(
|
||
[string]$Domain,
|
||
[string]$ValidationToken
|
||
)
|
||
|
||
Write-Log "=== DNS Challenge: Добавление TXT записи ===" "INFO"
|
||
|
||
# Извлекаем поддомен
|
||
$subdomain = "_acme-challenge"
|
||
|
||
# Добавляем TXT запись
|
||
$success = Add-TxtRecord -Domain $Script:Domain -Subdomain $subdomain -TxtValue $ValidationToken
|
||
|
||
if ($success) {
|
||
# Ждем распространения DNS
|
||
$waitTime = $Script:Config.DnsPropagationWait
|
||
Write-Log "Ожидание распространения DNS ($waitTime секунд)..."
|
||
Start-Sleep -Seconds $waitTime
|
||
|
||
Write-Log "DNS валидация готова" "SUCCESS"
|
||
return $true
|
||
}
|
||
|
||
return $false
|
||
}
|
||
|
||
function Invoke-DnsCleanup {
|
||
param(
|
||
[string]$Domain,
|
||
[string]$ValidationToken
|
||
)
|
||
|
||
Write-Log "=== DNS Challenge: Удаление TXT записи ===" "INFO"
|
||
|
||
$subdomain = "_acme-challenge"
|
||
|
||
return Remove-TxtRecord -Domain $Script:Domain -Subdomain $subdomain -TxtValue $ValidationToken
|
||
}
|
||
|
||
# ============================================================================
|
||
# ГЛАВНЫЕ ФУНКЦИИ
|
||
# ============================================================================
|
||
|
||
function Get-Certificate {
|
||
param([string]$Domain, [bool]$WildcardCert)
|
||
|
||
Write-Log "=== Запрос нового SSL сертификата ===" "INFO"
|
||
|
||
# Формируем список доменов
|
||
$domains = @($Domain)
|
||
if ($WildcardCert) {
|
||
$domains += "*.$Domain"
|
||
}
|
||
|
||
$domainArgs = @()
|
||
foreach ($d in $domains) {
|
||
$domainArgs += "-d"
|
||
$domainArgs += $d
|
||
}
|
||
|
||
Write-Log "Домены для сертификата: $($domains -join ', ')"
|
||
|
||
# Создаем временные скрипты для хуков
|
||
$authHookScript = Join-Path $env:TEMP "certbot_auth_hook.ps1"
|
||
$cleanupHookScript = Join-Path $env:TEMP "certbot_cleanup_hook.ps1"
|
||
|
||
# Скрипт для аутентификации
|
||
@"
|
||
param([string]`$Domain, [string]`$Token)
|
||
# Вызов функции добавления TXT записи
|
||
# Здесь должна быть логика работы с API reg.ru
|
||
"@ | Set-Content -Path $authHookScript
|
||
|
||
# Скрипт для очистки
|
||
@"
|
||
param([string]`$Domain, [string]`$Token)
|
||
# Вызов функции удаления TXT записи
|
||
"@ | Set-Content -Path $cleanupHookScript
|
||
|
||
Write-Log "Примечание: Для полной автоматизации используйте Linux версию скрипта" "WARNING"
|
||
Write-Log "На Windows рекомендуется использовать плагин certbot-dns-регru или выполнить вручную" "WARNING"
|
||
|
||
# Команда certbot (базовая, требует ручной DNS валидации)
|
||
$certbotArgs = @(
|
||
"certonly",
|
||
"--manual",
|
||
"--preferred-challenges", "dns",
|
||
"--email", $Script:Email,
|
||
"--agree-tos",
|
||
"--manual-public-ip-logging-ok"
|
||
) + $domainArgs
|
||
|
||
Write-Log "Запуск certbot..."
|
||
Write-Log "ВАЖНО: Certbot запросит вас добавить TXT записи в DNS" "WARNING"
|
||
Write-Log "Используйте API reg.ru или добавьте записи вручную через панель управления" "WARNING"
|
||
|
||
try {
|
||
& certbot @certbotArgs
|
||
|
||
Write-Log "Процесс получения сертификата завершен" "SUCCESS"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка при получении сертификата: $_" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Update-Certificate {
|
||
Write-Log "=== Обновление SSL сертификата ===" "INFO"
|
||
|
||
try {
|
||
& certbot renew
|
||
|
||
Write-Log "Проверка обновления завершена" "SUCCESS"
|
||
return $true
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка при обновлении: $_" "ERROR"
|
||
return $false
|
||
}
|
||
}
|
||
|
||
function Show-CertificateInfo {
|
||
param([string]$Domain)
|
||
|
||
$certPath = Join-Path $env:ProgramData "letsencrypt\live\$Domain\cert.pem"
|
||
|
||
if (-not (Test-Path $certPath)) {
|
||
Write-Log "Сертификат не найден" "WARNING"
|
||
return
|
||
}
|
||
|
||
Write-Log ("=" * 60)
|
||
Write-Log "ИНФОРМАЦИЯ О СЕРТИФИКАТЕ"
|
||
Write-Log ("=" * 60)
|
||
|
||
try {
|
||
$certInfo = & openssl x509 -in $certPath -text -noout
|
||
|
||
# Выводим основную информацию
|
||
$certInfo -split "`n" | Where-Object {
|
||
$_ -match "Subject:|Issuer:|Not Before|Not After|DNS:"
|
||
} | ForEach-Object {
|
||
Write-Log $_.Trim()
|
||
}
|
||
|
||
Write-Log ("=" * 60)
|
||
Write-Log "ПУТИ К ФАЙЛАМ СЕРТИФИКАТА:"
|
||
Write-Log " Сертификат: $certPath"
|
||
Write-Log " Приватный ключ: $(Join-Path $env:ProgramData "letsencrypt\live\$Domain\privkey.pem")"
|
||
Write-Log " Цепочка: $(Join-Path $env:ProgramData "letsencrypt\live\$Domain\chain.pem")"
|
||
Write-Log " Полная цепочка: $(Join-Path $env:ProgramData "letsencrypt\live\$Domain\fullchain.pem")"
|
||
Write-Log ("=" * 60)
|
||
}
|
||
catch {
|
||
Write-Log "Ошибка при чтении сертификата: $_" "ERROR"
|
||
}
|
||
}
|
||
|
||
# ============================================================================
|
||
# ОСНОВНАЯ ЛОГИКА
|
||
# ============================================================================
|
||
|
||
function Main {
|
||
Write-Log ("=" * 60)
|
||
Write-Log "СКРИПТ УПРАВЛЕНИЯ SSL СЕРТИФИКАТАМИ LET'S ENCRYPT"
|
||
Write-Log ("=" * 60)
|
||
|
||
# Проверка прав администратора
|
||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
|
||
if (-not $isAdmin) {
|
||
Write-Log "ПРЕДУПРЕЖДЕНИЕ: Скрипт запущен без прав администратора" "WARNING"
|
||
Write-Log "Некоторые операции могут потребовать повышенных прав" "WARNING"
|
||
}
|
||
|
||
# Загрузка конфигурации
|
||
if ($ConfigFile -and (Test-Path $ConfigFile)) {
|
||
Load-Configuration -ConfigPath $ConfigFile
|
||
}
|
||
|
||
# Проверка обязательных параметров
|
||
if (-not $Script:RegRuUsername -or -not $Script:RegRuPassword) {
|
||
Write-Log "ОШИБКА: Не указаны учетные данные reg.ru" "ERROR"
|
||
Write-Log "Укажите RegRuUsername и RegRuPassword или создайте файл конфигурации" "ERROR"
|
||
return
|
||
}
|
||
|
||
# Проверка Certbot
|
||
if (-not (Test-CertbotInstalled)) {
|
||
Write-Log "Установите Certbot и повторите попытку" "ERROR"
|
||
return
|
||
}
|
||
|
||
# Проверка срока действия сертификата
|
||
$daysLeft = Get-CertificateExpiry -Domain $Script:Domain
|
||
|
||
if ($null -eq $daysLeft) {
|
||
Write-Log "Сертификат не найден. Требуется создание нового." "INFO"
|
||
$success = Get-Certificate -Domain $Script:Domain -WildcardCert $Script:Wildcard
|
||
}
|
||
elseif ($daysLeft -lt 30) {
|
||
Write-Log "Сертификат истекает через $daysLeft дней. Требуется обновление!" "WARNING"
|
||
$success = Update-Certificate
|
||
}
|
||
else {
|
||
Write-Log "Сертификат действителен ($daysLeft дней)" "SUCCESS"
|
||
$success = $true
|
||
}
|
||
|
||
if ($success) {
|
||
Show-CertificateInfo -Domain $Script:Domain
|
||
}
|
||
|
||
Write-Log ("=" * 60)
|
||
Write-Log "Скрипт завершен"
|
||
Write-Log ("=" * 60)
|
||
}
|
||
|
||
# ============================================================================
|
||
# ТОЧКА ВХОДА
|
||
# ============================================================================
|
||
|
||
# Запуск основной функции
|
||
Main
|