Добавлен скрипт для автоматизации получения и обновления SSL сертификатов Let's Encrypt с использованием API reg.ru. Реализованы функции для работы с DNS-валидацией, логирования, проверки сертификатов и управления ими. Также создан bash-скрипт для упрощения процесса получения wildcard сертификата и обновления существующих сертификатов. Добавлена документация по настройке Nginx Proxy Manager с использованием полученного сертификата.

This commit is contained in:
Dmitriy Fofanov
2025-10-27 20:59:15 +03:00
parent f7de3d27b0
commit 07a65ffbba
8 changed files with 2498 additions and 51 deletions

556
letsencrypt_regru.ps1 Normal file
View File

@@ -0,0 +1,556 @@
# ============================================================================
# Скрипт для создания и обновления 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 "yyyy-MM-dd 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