don't use snakeoil certs in nginx (#10465)

this PR finally removes all uses of self-signed certificates from
certbot-nginx

i plan to create one last PR related to this deprecating
`acme.crypto_util.make_self_signed_cert` and moving the function to
certbot-compatibility-test which is the only place it's currently used

i think we could also do additional refactoring in certbot-nginx by
moving the _make_server_ssl call out of choose_or_make_vhost and make
deploy_cert responsible for calling it if the returned vhosts aren't
ssl. in this case, we could then skip updating cert and key directives a
second time as this is duplicate work if we just made the server ssl

i considered doing this, but it's a bigger refactor, breaks more tests,
and i'm not sure it really buys us much so i skipped it. i could take
this on or create an issue for it if you think it's important for us to
do for some reason tho ohemorange
This commit is contained in:
Brad Warren
2025-09-25 12:41:47 -07:00
committed by GitHub
parent 57d2429e4f
commit 513190afe0
3 changed files with 38 additions and 67 deletions
@@ -18,13 +18,8 @@ from typing import Sequence
from typing import Union from typing import Union
from typing import cast from typing import cast
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from acme import challenges from acme import challenges
from acme import crypto_util as acme_crypto_util
from certbot import achallenges from certbot import achallenges
from certbot import crypto_util
from certbot import errors from certbot import errors
from certbot import util from certbot import util
from certbot.compat import os from certbot.compat import os
@@ -254,7 +249,7 @@ class NginxConfigurator(common.Configurator):
"The nginx plugin currently requires --fullchain-path to " "The nginx plugin currently requires --fullchain-path to "
"install a certificate.") "install a certificate.")
vhosts = self.choose_or_make_vhosts(domain) vhosts = self.choose_or_make_vhosts(domain, key_path, fullchain_path)
for vhost in vhosts: for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path) self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
display_util.notify("Successfully deployed certificate for {} to {}" display_util.notify("Successfully deployed certificate for {} to {}"
@@ -358,7 +353,8 @@ class NginxConfigurator(common.Configurator):
""" """
return [vhost for vhost in self._choose_vhosts_common(target_name) if vhost.ssl] return [vhost for vhost in self._choose_vhosts_common(target_name) if vhost.ssl]
def choose_or_make_vhosts(self, target_name: str) -> list[obj.VirtualHost]: def choose_or_make_vhosts(self, target_name: str, key_path: str,
fullchain_path: str) -> list[obj.VirtualHost]:
"""Chooses or creates SSL virtual hosts based on the given domain name. """Chooses or creates SSL virtual hosts based on the given domain name.
If no matching vhost is found, we attempt to create a new one from the If no matching vhost is found, we attempt to create a new one from the
@@ -369,6 +365,8 @@ class NginxConfigurator(common.Configurator):
already SSL. already SSL.
:param str target_name: domain name :param str target_name: domain name
:param str key_path: key to use when creating SSL vhosts
:param str fullchain_path: certificates to use when creating SSL vhosts
:returns: ssl vhosts associated with name :returns: ssl vhosts associated with name
:rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost` :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost`
@@ -381,7 +379,7 @@ class NginxConfigurator(common.Configurator):
str(self.config.https_port))] str(self.config.https_port))]
for vhost in vhosts: for vhost in vhosts:
if not vhost.ssl: if not vhost.ssl:
self._make_server_ssl(vhost) self._make_server_ssl(vhost, key_path, fullchain_path)
return vhosts return vhosts
@@ -707,40 +705,16 @@ class NginxConfigurator(common.Configurator):
return util.get_filtered_names(all_names) return util.get_filtered_names(all_names)
def _get_snakeoil_paths(self) -> tuple[str, str]: def _make_server_ssl(self, vhost: obj.VirtualHost, key_path: str,
"""Generate invalid certs that let us create ssl directives for Nginx""" fullchain_path: str) -> None:
# TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
le_key = crypto_util.generate_key(
key_type='rsa', key_size=2048, key_dir=tmp_dir, keyname="key.pem",
strict_permissions=self.config.strict_permissions)
assert le_key.file is not None
cryptography_key = serialization.load_pem_private_key(le_key.pem, password=None)
assert isinstance(cryptography_key, rsa.RSAPrivateKey)
cert = acme_crypto_util.make_self_signed_cert(
cryptography_key,
# we used to use socket.gethostname here, but that sometimes
# resulted in strings over 64 characters long which would error
# on the validation introduced in
# https://github.com/pyca/cryptography/pull/11201. the ".invalid"
# TLD comes from RFC2606 (and was also used in the tls-sni-01
# challenge in early versions of the ACME spec)
domains=['temp-certbot-nginx.invalid']
)
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
cert_file, cert_path = util.unique_file(
os.path.join(tmp_dir, "cert.pem"), mode="wb")
with cert_file:
cert_file.write(cert_pem)
return cert_path, le_key.file
def _make_server_ssl(self, vhost: obj.VirtualHost) -> None:
"""Make a server SSL. """Make a server SSL.
Make a server SSL by adding new listen and SSL directives. Make a server SSL by adding new listen and SSL directives.
:param vhost: The vhost to add SSL to. :param vhost: The vhost to add SSL to.
:type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost` :type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost`
:param str key_path: key to use for SSL
:param str fullchain_path: certificates to use for SSL
""" """
https_port = self.config.https_port https_port = self.config.https_port
@@ -797,12 +771,10 @@ class NginxConfigurator(common.Configurator):
'ssl'] 'ssl']
addr_blocks.append(addr_block) addr_blocks.append(addr_block)
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = ([ ssl_block = ([
*addr_blocks, *addr_blocks,
['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate', ' ', fullchain_path],
['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'ssl_certificate_key', ' ', key_path],
['\n ', 'include', ' ', self.mod_ssl_conf], ['\n ', 'include', ' ', self.mod_ssl_conf],
['\n ', 'ssl_dhparam', ' ', self.ssl_dhparams], ['\n ', 'ssl_dhparam', ' ', self.ssl_dhparams],
]) ])
@@ -4,9 +4,6 @@ from unittest import mock
import pytest import pytest
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from acme import challenges from acme import challenges
from acme import messages from acme import messages
from certbot import achallenges from certbot import achallenges
@@ -182,7 +179,7 @@ class NginxConfiguratorTest(util.NginxTest):
'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"} 'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"}
conf_path = {key: os.path.normpath(value) for key, value in conf_path.items()} conf_path = {key: os.path.normpath(value) for key, value in conf_path.items()}
vhost = self.config.choose_or_make_vhosts(name)[0] vhost = self.config.choose_or_make_vhosts(name, 'key.pem', 'fullchain.pem')[0]
path = os.path.relpath(vhost.filep, self.temp_dir) path = os.path.relpath(vhost.filep, self.temp_dir)
assert conf_names[conf] == vhost.names assert conf_names[conf] == vhost.names
@@ -200,34 +197,51 @@ class NginxConfiguratorTest(util.NginxTest):
for name in bad_results: for name in bad_results:
with self.subTest(name=name): with self.subTest(name=name):
with pytest.raises(errors.MisconfigurationError): with pytest.raises(errors.MisconfigurationError):
self.config.choose_or_make_vhosts(name) self.config.choose_or_make_vhosts(name, 'key.pem', 'fullchain.pem')
def test_choose_or_make_vhosts_keep_ip_address(self): def test_choose_or_make_vhosts_keep_ip_address(self):
# let's use a simple helper function to set key and fullchain values
def choose_or_make_vhosts(domain):
return self.config.choose_or_make_vhosts(domain, 'key.pem', 'fullchain.pem')
# no listen on port 80 # no listen on port 80
# listen 69.50.225.155:9000; # listen 69.50.225.155:9000;
# listen 127.0.0.1; # listen 127.0.0.1;
vhost = self.config.choose_or_make_vhosts('example.com')[0] vhost = choose_or_make_vhosts('example.com')[0]
assert obj.Addr.fromstring("5001 ssl") in vhost.addrs assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
# no listens at all # no listens at all
vhost = self.config.choose_or_make_vhosts('no-listens.com')[0] vhost = choose_or_make_vhosts('no-listens.com')[0]
assert obj.Addr.fromstring("5001 ssl") in vhost.addrs assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
assert obj.Addr.fromstring("80") in vhost.addrs assert obj.Addr.fromstring("80") in vhost.addrs
# blank addr listen on 80 should result in blank addr ssl # blank addr listen on 80 should result in blank addr ssl
# listen 80; # listen 80;
# listen [::]:80; # listen [::]:80;
vhost = self.config.choose_or_make_vhosts('ipv6.com')[0] vhost = choose_or_make_vhosts('ipv6.com')[0]
assert obj.Addr.fromstring("5001 ssl") in vhost.addrs assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
assert obj.Addr.fromstring("[::]:5001 ssl") in vhost.addrs assert obj.Addr.fromstring("[::]:5001 ssl") in vhost.addrs
# listen on 80 with ip address should result in copied addr # listen on 80 with ip address should result in copied addr
# listen 1.2.3.4:80; # listen 1.2.3.4:80;
# listen [1:20::300]:80; # listen [1:20::300]:80;
vhost = self.config.choose_or_make_vhosts('addr-80.com')[0] vhost = choose_or_make_vhosts('addr-80.com')[0]
assert obj.Addr.fromstring("1.2.3.4:5001 ssl") in vhost.addrs assert obj.Addr.fromstring("1.2.3.4:5001 ssl") in vhost.addrs
assert obj.Addr.fromstring("[1:20::300]:5001 ssl ipv6only=on") in vhost.addrs assert obj.Addr.fromstring("[1:20::300]:5001 ssl ipv6only=on") in vhost.addrs
def test_choose_or_make_vhost_ssl_directives(self):
conf_path = self.config.parser.abs_path('sites-enabled/example.com')
self.config.choose_or_make_vhosts('example.com', 'my-key.pem', 'my-fullchain.pem')
self.config.save()
self.config.parser.load()
parsed_conf = util.filter_comments(self.config.parser.parsed[conf_path])
expected_directives = [
['ssl_certificate', 'my-fullchain.pem'],
['ssl_certificate_key', 'my-key.pem'],
]
for directive in expected_directives:
assert util.contains_at_depth(parsed_conf, directive, 2)
def test_ipv6only(self): def test_ipv6only(self):
# ipv6_info: (ipv6_active, ipv6only_present) # ipv6_info: (ipv6_active, ipv6only_present)
@@ -238,14 +252,8 @@ class NginxConfiguratorTest(util.NginxTest):
def test_ipv6only_detection(self): def test_ipv6only_detection(self):
self.config.version = (1, 3, 1) self.config.version = (1, 3, 1)
self.config.deploy_cert( vhost = self.config.choose_or_make_vhosts("ipv6.com", "key.pem", "fullchain.pem")[0]
"ipv6.com", for addr in vhost.addrs:
"example/cert.pem",
"example/key.pem",
"example/chain.pem",
"example/fullchain.pem")
for addr in self.config.choose_or_make_vhosts("ipv6.com")[0].addrs:
assert not addr.ipv6only assert not addr.ipv6only
def test_more_info(self): def test_more_info(self):
@@ -570,16 +578,6 @@ class NginxConfiguratorTest(util.NginxTest):
with pytest.raises(errors.PluginError): with pytest.raises(errors.PluginError):
self.config.save() self.config.save()
def test_get_snakeoil_paths(self):
# pylint: disable=protected-access
cert, key = self.config._get_snakeoil_paths()
assert os.path.exists(cert)
assert os.path.exists(key)
with open(cert, "rb") as cert_file:
x509.load_pem_x509_certificate(cert_file.read())
with open(key, "rb") as key_file:
serialization.load_pem_private_key(key_file.read(), password=None)
def test_redirect_enhance(self): def test_redirect_enhance(self):
# Test that we successfully add a redirect when there is # Test that we successfully add a redirect when there is
# a listen directive # a listen directive
+1
View File
@@ -0,0 +1 @@
certbot-nginx no longer creates and uses self-signed certificates as an intermediate step when installing certificates. The certificates the user requested Certbot install are now always used instead.