Hi guys, I tried to search the forum but it is not clear to me if anyone has the same problem and has found a solution.
The problem is that even after the LetsEncrypt SSL certificate has expired and has been successfully renewed by automation, a host reboot is still required. Only after the reboot is the host reachable via https from the internet again. Anyone have the same problem, can you explain to me how it solved?
No, you’re not alone. I have automated my LetsEncrypt (DNS auth method) but even though I have the certificate being renewed, it still isn’t picked up by HA.
I even added these lines at the end of the LetsEncrypt renewal cleanup script:
systemctl stop home-assistant
sleep 10
systemctl start home-assistant
Interestingly, if the above command is run once by certbot, HA doesn’t recognise the updated certificate. But if I SSH in after the renewal and run the exact commands again, HA picks it up.
I’m going to keep working on this. I’m going to try a scripted host reboot next, but that’s not my preferred outcome.
Did you happen to find a better way than a host reboot?
A workaround will be to use nginx proxy (as add-on, for example) for SSL and not to use SSL in HA. Then after cert renew only nginx should be restarted.
Finally, I solved it by creating a value template that counts the seconds that are missing until the certificate expires:
ssl_cert_expiry_seconds:
entity_id: sensor.cert_expiry_timestamp_xxxxxxxx
friendly_name: 'friendly name'
value_template: '{{ ((as_timestamp(states("sensor.cert_expiry_timestamp_xxxxxxxx")[:16].replace("T"," ")) - as_timestamp(now()))) }}'
unit_of_measurement: seconds
After, automation that in the order performs the renewal of the certificate and a reboot of the host within a delay to allow the correct renewal of the certificate before the reboot.
- id: NotifyCerticateExpired
alias: Notify Certicate Expired
description: ''
trigger:
- platform: state
entity_id: sensor.ssl_cert_expiry_seconds
condition:
- condition: numeric_state
entity_id: sensor.ssl_cert_expiry_seconds
below: '0'
action:
- device_id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
domain: mobile_app
type: notify
message: Certificato SSL Scaduto, necessario RESTART dell'host
title: HOME - Let's Encrypt
- service: hassio.addon_restart
data:
addon: core_letsencrypt
- delay:
hours: 0
minutes: 1
seconds: 0
milliseconds: 0
- service: homeassistant.restart
There is probably a better way, but at the moment I don’t see it around and even the one suggested to go through a proxy is not clear to me.
I have achieved this by modifying the source code of home assistant http component.
I’m running an older version of home assistant, so the code probably won’t quite match what’s in the github repository.
I modified the components/http/__init__.py file:
async def start(self) -> None:
"""Start the aiohttp server."""
self.ssl_context = await self.create_ssl_context()
# Aiohttp freezes apps after start so that no changes can be made.
# However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError.
# To work around this we now prevent the router from getting frozen
# pylint: disable=protected-access
self.app._router.freeze = lambda: None # type: ignore[assignment]
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = HomeAssistantTCPSite(
self.runner, self.server_host, self.server_port, ssl_context=self.ssl_context
)
try:
if(self.ssl_context):
loop = asyncio.get_event_loop()
task = loop.create_task(self.monitor_ssl_certificate())
await self.site.start()
except OSError as error:
_LOGGER.error(
"Failed to create HTTP server at port %d: %s", self.server_port, error
)
_LOGGER.info("Now listening on port %d", self.server_port)
async def create_ssl_context(self) -> ssl.SSLContext | None:
context: ssl.SSLContext | None
if self.ssl_certificate:
try:
if self.ssl_profile == SSL_INTERMEDIATE:
context = ssl_util.server_context_intermediate()
else:
context = ssl_util.server_context_modern()
await self.load_ssl_certificate(context)
except OSError as error:
_LOGGER.error(
"Could not read SSL certificate from %s: %s",
self.ssl_certificate,
error,
)
return
else:
context = None
return context
async def load_ssl_certificate(self, context: ssl.SSLContext) -> None:
try:
await self.hass.async_add_executor_job(
context.load_cert_chain, self.ssl_certificate, self.ssl_key
)
except OSError as error:
_LOGGER.error(
"Could not read SSL certificate from %s: %s",
self.ssl_certificate,
error,
)
if self.ssl_peer_certificate:
context.verify_mode = ssl.CERT_REQUIRED
await self.hass.async_add_executor_job(
context.load_verify_locations, self.ssl_peer_certificate
)
return
async def monitor_ssl_certificate(self) -> None:
_LOGGER.info("Starting SSL Certificate monitoring")
if(self.ssl_context):
prevHash: str
with open(self.ssl_certificate, "rb") as file:
prevHash = hashlib.md5(file.read()).hexdigest()
while True:
try:
with open(self.ssl_certificate, "rb") as file:
currentHash = hashlib.md5(file.read()).hexdigest()
if(prevHash != currentHash):
prevHash = currentHash
_LOGGER.info("Reloading SSL Certificate")
await self.load_ssl_certificate(self.ssl_context)
except Exception as error:
_LOGGER.error(
"Failed to reload SSL certificate from %s: %s",
self.ssl_certificate,
error
)
await asyncio.sleep(60)
This checks if the certificate has changed every 60 seconds and the loads certificate into the SSLContext object that was used to start the http server.
I’m not sure if some other components require reloading the certificate chain, but this works for my needs.