Увеличены значения параметров ожидания и проверки DNS в конфигурации и документации
Синхронизация Wiki / Синхронизация Wiki (push) Successful in 2s
Синхронизация Wiki / Синхронизация Wiki (push) Successful in 2s
This commit is contained in:
@@ -154,9 +154,9 @@ jobs:
|
|||||||
"email": os.environ["CFG_EMAIL"],
|
"email": os.environ["CFG_EMAIL"],
|
||||||
"cert_dir": os.environ.get("CFG_CERT_DIR") or "/etc/letsencrypt/live",
|
"cert_dir": os.environ.get("CFG_CERT_DIR") or "/etc/letsencrypt/live",
|
||||||
"log_file": os.environ.get("CFG_LOG_FILE") or "/var/log/letsencrypt_regru.log",
|
"log_file": os.environ.get("CFG_LOG_FILE") or "/var/log/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": to_int(os.environ.get("CFG_DNS_PROPAGATION_WAIT"), 60),
|
"dns_propagation_wait": to_int(os.environ.get("CFG_DNS_PROPAGATION_WAIT"), 180),
|
||||||
"dns_check_attempts": to_int(os.environ.get("CFG_DNS_CHECK_ATTEMPTS"), 10),
|
"dns_check_attempts": to_int(os.environ.get("CFG_DNS_CHECK_ATTEMPTS"), 20),
|
||||||
"dns_check_interval": to_int(os.environ.get("CFG_DNS_CHECK_INTERVAL"), 10),
|
"dns_check_interval": to_int(os.environ.get("CFG_DNS_CHECK_INTERVAL"), 15),
|
||||||
"renewal_days": to_int(os.environ.get("CFG_RENEWAL_DAYS"), 30),
|
"renewal_days": to_int(os.environ.get("CFG_RENEWAL_DAYS"), 30),
|
||||||
"npm_enabled": to_bool(os.environ.get("CFG_NPM_ENABLED"), True),
|
"npm_enabled": to_bool(os.environ.get("CFG_NPM_ENABLED"), True),
|
||||||
"npm_host": os.environ.get("CFG_NPM_HOST") or "",
|
"npm_host": os.environ.get("CFG_NPM_HOST") or "",
|
||||||
|
|||||||
@@ -711,9 +711,9 @@ sudo nano /etc/letsencrypt/regru_config.json
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"cert_dir": "/etc/letsencrypt/live",
|
"cert_dir": "/etc/letsencrypt/live",
|
||||||
"log_file": "/var/log/letsencrypt_regru.log",
|
"log_file": "/var/log/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": 60,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 10,
|
"dns_check_attempts": 20,
|
||||||
"dns_check_interval": 10
|
"dns_check_interval": 15
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -10,9 +10,9 @@
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"cert_dir": "/etc/letsencrypt/live",
|
"cert_dir": "/etc/letsencrypt/live",
|
||||||
"log_file": "/var/log/letsencrypt_regru.log",
|
"log_file": "/var/log/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": 60,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 10,
|
"dns_check_attempts": 20,
|
||||||
"dns_check_interval": 10,
|
"dns_check_interval": 15,
|
||||||
"renewal_days": 30,
|
"renewal_days": 30,
|
||||||
"npm_enabled": true,
|
"npm_enabled": true,
|
||||||
"npm_host": "http://192.0.2.1:81",
|
"npm_host": "http://192.0.2.1:81",
|
||||||
|
|||||||
@@ -141,9 +141,9 @@ function Create-SampleConfig {
|
|||||||
domain = "example.com"
|
domain = "example.com"
|
||||||
wildcard = $true
|
wildcard = $true
|
||||||
email = "admin@example.com"
|
email = "admin@example.com"
|
||||||
dns_propagation_wait = 60
|
dns_propagation_wait = 180
|
||||||
dns_check_attempts = 10
|
dns_check_attempts = 20
|
||||||
dns_check_interval = 10
|
dns_check_interval = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
$sampleConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath
|
$sampleConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath
|
||||||
|
|||||||
@@ -340,9 +340,9 @@ create_config() {
|
|||||||
"email": "${email}",
|
"email": "${email}",
|
||||||
"cert_dir": "${CERT_DIR}",
|
"cert_dir": "${CERT_DIR}",
|
||||||
"log_file": "${LOG_DIR}/letsencrypt_regru.log",
|
"log_file": "${LOG_DIR}/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": 60,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 10,
|
"dns_check_attempts": 20,
|
||||||
"dns_check_interval": 10,
|
"dns_check_interval": 15,
|
||||||
"renewal_days": 30,
|
"renewal_days": 30,
|
||||||
"npm_enabled": ${npm_enabled},
|
"npm_enabled": ${npm_enabled},
|
||||||
"npm_host": "${npm_host}",
|
"npm_host": "${npm_host}",
|
||||||
|
|||||||
+120
-23
@@ -75,9 +75,9 @@ DEFAULT_CONFIG = {
|
|||||||
"log_file": "/var/log/letsencrypt_regru.log",
|
"log_file": "/var/log/letsencrypt_regru.log",
|
||||||
|
|
||||||
# Параметры DNS
|
# Параметры DNS
|
||||||
"dns_propagation_wait": 60, # Время ожидания распространения DNS (секунды)
|
"dns_propagation_wait": 180, # Время ожидания распространения DNS (секунды)
|
||||||
"dns_check_attempts": 10, # Количество попыток проверки DNS
|
"dns_check_attempts": 20, # Количество попыток проверки DNS
|
||||||
"dns_check_interval": 10, # Интервал между проверками DNS (секунды)
|
"dns_check_interval": 15, # Интервал между проверками DNS (секунды)
|
||||||
|
|
||||||
# Параметры обновления сертификата
|
# Параметры обновления сертификата
|
||||||
"renewal_days": 30, # За сколько дней до истечения обновлять (по умолчанию 30)
|
"renewal_days": 30, # За сколько дней до истечения обновлять (по умолчанию 30)
|
||||||
@@ -259,34 +259,42 @@ class RegRuAPI:
|
|||||||
self.logger.error(f"Ошибка HTTP запроса: {e}")
|
self.logger.error(f"Ошибка HTTP запроса: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_zone_records(self, domain: str) -> List[Dict]:
|
def get_zone_records(self, domain: str, retries: int = 3) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Получение DNS записей домена
|
Получение DNS записей домена (с повторами при пустом ответе)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
domain: Доменное имя
|
domain: Доменное имя
|
||||||
|
retries: Количество повторных попыток при пустом ответе
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список DNS записей
|
Список DNS записей
|
||||||
"""
|
"""
|
||||||
self.logger.info(f"Получение DNS записей для домена: {domain}")
|
self.logger.info(f"Получение DNS записей для домена: {domain}")
|
||||||
|
|
||||||
# Задержка перед запросом (защита от rate limit)
|
for attempt in range(retries):
|
||||||
time.sleep(1)
|
# Задержка перед запросом (защита от rate limit)
|
||||||
|
time.sleep(2 if attempt > 0 else 1)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"domain_name": domain,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self._make_request("zone/get_resource_records", params)
|
||||||
|
|
||||||
|
if "answer" in result and "records" in result["answer"]:
|
||||||
|
records = result["answer"]["records"]
|
||||||
|
self.logger.info(f"Получено {len(records)} DNS записей")
|
||||||
|
return records
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Ошибка при получении DNS записей (попытка {attempt + 1}/{retries}): {e}")
|
||||||
|
|
||||||
|
if attempt < retries - 1:
|
||||||
|
self.logger.info(f"DNS записи не получены, повтор ({attempt + 2}/{retries})...")
|
||||||
|
|
||||||
params = {
|
self.logger.warning("DNS записи не найдены после всех попыток")
|
||||||
"domain_name": domain,
|
return []
|
||||||
}
|
|
||||||
|
|
||||||
result = self._make_request("zone/get_resource_records", params)
|
|
||||||
|
|
||||||
if "answer" in result and "records" in result["answer"]:
|
|
||||||
records = result["answer"]["records"]
|
|
||||||
self.logger.info(f"Получено {len(records)} DNS записей")
|
|
||||||
return records
|
|
||||||
else:
|
|
||||||
self.logger.warning("DNS записи не найдены")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def add_txt_record(self, domain: str, subdomain: str, txt_value: str) -> bool:
|
def add_txt_record(self, domain: str, subdomain: str, txt_value: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -1354,7 +1362,7 @@ class LetsEncryptManager:
|
|||||||
self.logger.info("✅ TXT запись успешно добавлена в API reg.ru")
|
self.logger.info("✅ TXT запись успешно добавлена в API reg.ru")
|
||||||
|
|
||||||
# Ждем распространения DNS
|
# Ждем распространения DNS
|
||||||
wait_time = self.config.get("dns_propagation_wait", 60)
|
wait_time = self.config.get("dns_propagation_wait", 120)
|
||||||
self.logger.info("")
|
self.logger.info("")
|
||||||
self.logger.info("⏳ Ожидание распространения DNS...")
|
self.logger.info("⏳ Ожидание распространения DNS...")
|
||||||
self.logger.info(f" Время ожидания: {wait_time} секунд")
|
self.logger.info(f" Время ожидания: {wait_time} секунд")
|
||||||
@@ -1394,7 +1402,10 @@ class LetsEncryptManager:
|
|||||||
|
|
||||||
def dns_cleanup_hook(self, validation_domain: str, validation_token: str) -> bool:
|
def dns_cleanup_hook(self, validation_domain: str, validation_token: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Обработчик очистки DNS challenge - удаление TXT записи
|
Обработчик очистки DNS challenge - удаление ВСЕХ TXT записей для данного subdomain.
|
||||||
|
|
||||||
|
Удаляет ВСЕ _acme-challenge TXT записи (не только с совпадающим токеном),
|
||||||
|
чтобы не оставлять stale записей от параллельных wildcard/apex challenge'ей.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
validation_domain: Домен валидации (например, example.com или *.example.com)
|
validation_domain: Домен валидации (например, example.com или *.example.com)
|
||||||
@@ -1414,7 +1425,36 @@ class LetsEncryptManager:
|
|||||||
|
|
||||||
self.logger.info(f"Домен: {registrable_domain}, Поддомен: {subdomain}")
|
self.logger.info(f"Домен: {registrable_domain}, Поддомен: {subdomain}")
|
||||||
|
|
||||||
return self.api.remove_txt_record(registrable_domain, subdomain, validation_token)
|
# Удаляем ВСЕ TXT записи с этим subdomain (а не только совпадающие по значению)
|
||||||
|
try:
|
||||||
|
records = self.api.get_zone_records(registrable_domain)
|
||||||
|
matching = [
|
||||||
|
r for r in records
|
||||||
|
if r.get("rectype") == "TXT"
|
||||||
|
and r.get("subdomain", "").lower() == subdomain.lower()
|
||||||
|
]
|
||||||
|
if not matching:
|
||||||
|
self.logger.info("TXT записи для удаления не найдены")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.logger.info(f"Найдено {len(matching)} TXT записей для удаления")
|
||||||
|
for record in matching:
|
||||||
|
record_id = record.get("id")
|
||||||
|
record_text = record.get("text", "")[:30]
|
||||||
|
try:
|
||||||
|
self.api._make_request("zone/remove_record", {
|
||||||
|
"domain_name": registrable_domain,
|
||||||
|
"record_id": record_id,
|
||||||
|
})
|
||||||
|
self.logger.info(f"Удалена TXT запись ID={record_id} ({record_text}...)")
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Не удалось удалить запись {record_id}: {e}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Ошибка при очистке TXT записей: {e}")
|
||||||
|
# Fallback: пытаемся удалить конкретную запись по значению
|
||||||
|
return self.api.remove_txt_record(registrable_domain, subdomain, validation_token)
|
||||||
|
|
||||||
def _get_authoritative_nameservers(self, domain: str) -> List[str]:
|
def _get_authoritative_nameservers(self, domain: str) -> List[str]:
|
||||||
"""Возвращает список authoritative NS для домена."""
|
"""Возвращает список authoritative NS для домена."""
|
||||||
@@ -1563,6 +1603,59 @@ class LetsEncryptManager:
|
|||||||
"""
|
"""
|
||||||
return self.verify_dns_record_external(self.domain, subdomain, expected_value)
|
return self.verify_dns_record_external(self.domain, subdomain, expected_value)
|
||||||
|
|
||||||
|
def _clean_stale_acme_records(self, domains: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Удаляет ВСЕ stale _acme-challenge TXT записи для указанных доменов
|
||||||
|
ПЕРЕД запуском certbot. Это предотвращает ситуацию, когда старые
|
||||||
|
токены от предыдущих неудачных попыток мешают валидации.
|
||||||
|
"""
|
||||||
|
configured_domains = get_configured_domains(self.config)
|
||||||
|
|
||||||
|
# Собираем уникальные (registrable_domain, subdomain) пары
|
||||||
|
pairs_seen = set()
|
||||||
|
pairs = []
|
||||||
|
for domain in domains:
|
||||||
|
registrable_domain, subdomain_prefix = extract_registrable_domain(
|
||||||
|
domain, configured_domains
|
||||||
|
)
|
||||||
|
subdomain = f"_acme-challenge.{subdomain_prefix}" if subdomain_prefix else "_acme-challenge"
|
||||||
|
key = (registrable_domain.lower(), subdomain.lower())
|
||||||
|
if key not in pairs_seen:
|
||||||
|
pairs_seen.add(key)
|
||||||
|
pairs.append((registrable_domain, subdomain))
|
||||||
|
|
||||||
|
# Для каждого уникального registrable_domain — одна загрузка записей
|
||||||
|
zone_cache: Dict[str, List[Dict]] = {}
|
||||||
|
for registrable_domain, subdomain in pairs:
|
||||||
|
rd_lower = registrable_domain.lower()
|
||||||
|
if rd_lower not in zone_cache:
|
||||||
|
try:
|
||||||
|
zone_cache[rd_lower] = self.api.get_zone_records(registrable_domain)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Не удалось получить DNS записи {registrable_domain}: {e}")
|
||||||
|
zone_cache[rd_lower] = []
|
||||||
|
|
||||||
|
records = zone_cache[rd_lower]
|
||||||
|
stale_ids = [
|
||||||
|
r.get("id") for r in records
|
||||||
|
if r.get("rectype") == "TXT"
|
||||||
|
and r.get("subdomain", "").lower() == subdomain.lower()
|
||||||
|
and r.get("id")
|
||||||
|
]
|
||||||
|
|
||||||
|
if stale_ids:
|
||||||
|
self.logger.info(f"Удаление {len(stale_ids)} stale _acme-challenge TXT записей "
|
||||||
|
f"({subdomain}.{registrable_domain})")
|
||||||
|
for record_id in stale_ids:
|
||||||
|
try:
|
||||||
|
self.api._make_request("zone/remove_record", {
|
||||||
|
"domain_name": registrable_domain,
|
||||||
|
"record_id": record_id,
|
||||||
|
})
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Не удалось удалить запись {record_id}: {e}")
|
||||||
|
|
||||||
def obtain_certificate(self, staging: bool = False) -> bool:
|
def obtain_certificate(self, staging: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Получение нового сертификата
|
Получение нового сертификата
|
||||||
@@ -1617,6 +1710,10 @@ class LetsEncryptManager:
|
|||||||
for d in domains:
|
for d in domains:
|
||||||
domain_args.extend(["-d", d])
|
domain_args.extend(["-d", d])
|
||||||
|
|
||||||
|
# Удаляем stale _acme-challenge TXT записи от предыдущих неудачных попыток
|
||||||
|
self.logger.info("Очистка stale ACME TXT записей перед запуском certbot...")
|
||||||
|
self._clean_stale_acme_records(domains)
|
||||||
|
|
||||||
# Создаём временные wrapper скрипты для hooks
|
# Создаём временные wrapper скрипты для hooks
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"cert_dir": "/etc/letsencrypt/live",
|
"cert_dir": "/etc/letsencrypt/live",
|
||||||
"log_file": "/var/log/letsencrypt_regru.log",
|
"log_file": "/var/log/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": 60,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 10,
|
"dns_check_attempts": 20,
|
||||||
"dns_check_interval": 10,
|
"dns_check_interval": 15,
|
||||||
"renewal_days": 30,
|
"renewal_days": 30,
|
||||||
"npm_enabled": true,
|
"npm_enabled": true,
|
||||||
"npm_host": "http://192.0.2.1:81",
|
"npm_host": "http://192.0.2.1:81",
|
||||||
|
|||||||
@@ -286,9 +286,9 @@ sudo systemctl restart letsencrypt-regru.timer
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"cert_dir": "/etc/letsencrypt/live",
|
"cert_dir": "/etc/letsencrypt/live",
|
||||||
"log_file": "/var/log/letsencrypt-regru/letsencrypt_regru.log",
|
"log_file": "/var/log/letsencrypt-regru/letsencrypt_regru.log",
|
||||||
"dns_propagation_wait": 60,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 10,
|
"dns_check_attempts": 20,
|
||||||
"dns_check_interval": 10,
|
"dns_check_interval": 15,
|
||||||
"renewal_days": 30,
|
"renewal_days": 30,
|
||||||
"npm_enabled": true,
|
"npm_enabled": true,
|
||||||
"npm_host": "http://10.10.10.14:81",
|
"npm_host": "http://10.10.10.14:81",
|
||||||
@@ -399,7 +399,7 @@ letsencrypt-regru --obtain -v
|
|||||||
**DNS не обновляется**
|
**DNS не обновляется**
|
||||||
```bash
|
```bash
|
||||||
## Увеличьте время ожидания в config.json
|
## Увеличьте время ожидания в config.json
|
||||||
"dns_propagation_wait": 120,
|
"dns_propagation_wait": 180,
|
||||||
"dns_check_attempts": 20
|
"dns_check_attempts": 20
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user