Preserve IP addresses when adding ssl listen directives in nginx server blocks (#10145)

Fixes #10011

When we take a server block with no ssl addresses in and and enable ssl,
if it has any listens on the http port, use those host addresses when
creating a directive to listen on ssl. Addresses with no port and on
other ports will be ignored.

---------

Co-authored-by: Will Greenberg <willg@eff.org>
This commit is contained in:
ohemorange
2025-01-28 10:52:06 -08:00
committed by GitHub
co-authored by Will Greenberg
parent 70ba4f2438
commit d98edd97ad
6 changed files with 96 additions and 37 deletions
@@ -21,6 +21,7 @@ from typing import Set
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import Union from typing import Union
from typing import cast
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
@@ -371,13 +372,14 @@ class NginxConfigurator(common.Configurator):
return vhosts return vhosts
def ipv6_info(self, port: str) -> Tuple[bool, bool]: def ipv6_info(self, host: str, port: str) -> Tuple[bool, bool]:
"""Returns tuple of booleans (ipv6_active, ipv6only_present) """Returns tuple of booleans (ipv6_active, ipv6only_present)
ipv6_active is true if any server block listens ipv6 address in any port ipv6_active is true if any server block listens ipv6 address in any port
ipv6only_present is true if ipv6only=on option exists in any server ipv6only_present is true if ipv6only=on option exists in any server
block ipv6 listen directive for the specified port. block ipv6 listen directive for the specified port.
:param str host: Host to check ipv6only=on directive for
:param str port: Port to check ipv6only=on directive for :param str port: Port to check ipv6only=on directive for
:returns: Tuple containing information if IPv6 is enabled in the global :returns: Tuple containing information if IPv6 is enabled in the global
@@ -391,7 +393,7 @@ class NginxConfigurator(common.Configurator):
for addr in vh.addrs: for addr in vh.addrs:
if addr.ipv6: if addr.ipv6:
ipv6_active = True ipv6_active = True
if addr.ipv6only and addr.get_port() == port: if addr.ipv6only and addr.get_port() == port and addr.get_addr() == host:
ipv6only_present = True ipv6only_present = True
return ipv6_active, ipv6only_present return ipv6_active, ipv6only_present
@@ -723,9 +725,16 @@ class NginxConfigurator(common.Configurator):
""" """
https_port = self.config.https_port https_port = self.config.https_port
ipv6info = self.ipv6_info(str(https_port)) http_port = self.config.http01_port
ipv6_block = ['']
ipv4_block = [''] # no addresses should have ssl turned on here
assert not vhost.ssl
addrs_to_insert: List[obj.Addr] = [
cast(obj.Addr, obj.Addr.fromstring(f'{addr.get_addr()}:{https_port} ssl'))
for addr in vhost.addrs
if addr.get_port() == str(http_port)
]
# If the vhost was implicitly listening on the default Nginx port, # If the vhost was implicitly listening on the default Nginx port,
# have it continue to do so. # have it continue to do so.
@@ -733,31 +742,46 @@ class NginxConfigurator(common.Configurator):
listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]] listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]
self.parser.add_server_directives(vhost, listen_block) self.parser.add_server_directives(vhost, listen_block)
if vhost.ipv6_enabled(): if not addrs_to_insert:
ipv6_block = ['\n ', # there are no existing addresses listening on 80
'listen', if vhost.ipv6_enabled():
' ', addrs_to_insert += [cast(obj.Addr, obj.Addr.fromstring(f'[::]:{https_port} ssl'))]
'[::]:{0}'.format(https_port), if vhost.ipv4_enabled():
' ', addrs_to_insert += [cast(obj.Addr, obj.Addr.fromstring(f'{https_port} ssl'))]
'ssl']
if not ipv6info[1]:
# ipv6only=on is absent in global config
ipv6_block.append(' ')
ipv6_block.append('ipv6only=on')
if vhost.ipv4_enabled(): addr_blocks: List[List[str]] = []
ipv4_block = ['\n ', ipv6only_set_here: Set[Tuple[str, str]] = set()
'listen', for addr in addrs_to_insert:
' ', host = addr.get_addr()
'{0}'.format(https_port), port = addr.get_port()
' ', if addr.ipv6:
'ssl'] addr_block = ['\n ',
'listen',
' ',
f'{host}:{port}',
' ',
'ssl']
ipv6only_exists = self.ipv6_info(host, port)[1]
if not ipv6only_exists and (host, port) not in ipv6only_set_here:
addr.ipv6only = True # bookkeeping in case we switch output implementation
ipv6only_set_here.add((host, port))
addr_block.append(' ')
addr_block.append('ipv6only=on')
addr_blocks.append(addr_block)
else:
tuple_string = f'{host}:{port}' if host else f'{port}'
addr_block = ['\n ',
'listen',
' ',
tuple_string,
' ',
'ssl']
addr_blocks.append(addr_block)
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths() snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = ([ ssl_block = ([
ipv6_block, *addr_blocks,
ipv4_block,
['\n ', 'ssl_certificate', ' ', snakeoil_cert], ['\n ', 'ssl_certificate', ' ', snakeoil_cert],
['\n ', 'ssl_certificate_key', ' ', snakeoil_key], ['\n ', 'ssl_certificate_key', ' ', snakeoil_key],
['\n ', 'include', ' ', self.mod_ssl_conf], ['\n ', 'include', ' ', self.mod_ssl_conf],
@@ -152,7 +152,7 @@ class NginxHttp01(common.ChallengePerformer):
self.configurator.config.http01_port) self.configurator.config.http01_port)
port = self.configurator.config.http01_port port = self.configurator.config.http01_port
ipv6, ipv6only = self.configurator.ipv6_info(str(port)) ipv6, ipv6only = self.configurator.ipv6_info("[::]", str(port))
if ipv6: if ipv6:
# If IPv6 is active in Nginx configuration # If IPv6 is active in Nginx configuration
@@ -42,7 +42,7 @@ class NginxConfiguratorTest(util.NginxTest):
def test_prepare(self): def test_prepare(self):
assert (1, 6, 2) == self.config.version assert (1, 6, 2) == self.config.version
assert 14 == len(self.config.parser.parsed) assert 15 == len(self.config.parser.parsed)
@mock.patch("certbot_nginx._internal.configurator.util.exe_exists") @mock.patch("certbot_nginx._internal.configurator.util.exe_exists")
@mock.patch("certbot_nginx._internal.configurator.subprocess.run") @mock.patch("certbot_nginx._internal.configurator.subprocess.run")
@@ -100,7 +100,7 @@ class NginxConfiguratorTest(util.NginxTest):
"155.225.50.69.nephoscale.net", "www.example.org", "another.alias", "155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
"migration.com", "summer.com", "geese.com", "sslon.com", "migration.com", "summer.com", "geese.com", "sslon.com",
"globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com", "globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com",
"headers.com", "example.net", "ssl.both.com"} "headers.com", "example.net", "ssl.both.com", 'addr-80.com'}
def test_supported_enhancements(self): def test_supported_enhancements(self):
assert ['redirect', 'ensure-http-header', 'staple-ocsp'] == \ assert ['redirect', 'ensure-http-header', 'staple-ocsp'] == \
@@ -201,11 +201,38 @@ class NginxConfiguratorTest(util.NginxTest):
with pytest.raises(errors.MisconfigurationError): with pytest.raises(errors.MisconfigurationError):
self.config.choose_vhosts(name) self.config.choose_vhosts(name)
def test_choose_vhosts_keep_ip_address(self):
# no listen on port 80
# listen 69.50.225.155:9000;
# listen 127.0.0.1;
vhost = self.config.choose_vhosts('example.com')[0]
assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
# no listens at all
vhost = self.config.choose_vhosts('headers.com')[0]
assert obj.Addr.fromstring("5001 ssl") in vhost.addrs
assert obj.Addr.fromstring("80") in vhost.addrs
# blank addr listen on 80 should result in blank addr ssl
# listen 80;
# listen [::]:80;
vhost = self.config.choose_vhosts('ipv6.com')[0]
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 1.2.3.4:80;
# listen [1:20::300]:80;
vhost = self.config.choose_vhosts('addr-80.com')[0]
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
def test_ipv6only(self): def test_ipv6only(self):
# ipv6_info: (ipv6_active, ipv6only_present) # ipv6_info: (ipv6_active, ipv6only_present)
assert (True, False) == self.config.ipv6_info("80") assert (True, False) == self.config.ipv6_info("[::]", "80")
# Port 443 has ipv6only=on because of ipv6ssl.com vhost # Port 443 has ipv6only=on because of ipv6ssl.com vhost
assert (True, True) == self.config.ipv6_info("443") assert (True, True) == self.config.ipv6_info("[::]", "443")
def test_ipv6only_detection(self): def test_ipv6only_detection(self):
self.config.version = (1, 3, 1) self.config.version = (1, 3, 1)
@@ -585,7 +612,7 @@ class NginxConfiguratorTest(util.NginxTest):
generated_conf = self.config.parser.parsed[example_conf] generated_conf = self.config.parser.parsed[example_conf]
assert [[['server'], [ assert [[['server'], [
['server_name', '.example.com'], ['server_name', '.example.com'],
['server_name', 'example.*'], [], ['server_name', 'example.*'],
['listen', '5001', 'ssl'], ['#', ' managed by Certbot'], ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'], ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],
['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'], ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],
@@ -615,7 +642,7 @@ class NginxConfiguratorTest(util.NginxTest):
generated_conf = self.config.parser.parsed[example_conf] generated_conf = self.config.parser.parsed[example_conf]
assert [[['server'], [ assert [[['server'], [
['server_name', '.example.com'], ['server_name', '.example.com'],
['server_name', 'example.*'], [], ['server_name', 'example.*'],
['listen', '5001', 'ssl'], ['#', ' managed by Certbot'], ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'], ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],
['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'], ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],
@@ -630,7 +657,7 @@ class NginxConfiguratorTest(util.NginxTest):
['listen', '127.0.0.1'], ['listen', '127.0.0.1'],
['server_name', '.example.com'], ['server_name', '.example.com'],
['server_name', 'example.*'], ['server_name', 'example.*'],
[], [], []]]] == \ [], []]]] == \
generated_conf generated_conf
def test_http_header_hsts(self): def test_http_header_hsts(self):
@@ -957,7 +984,7 @@ class NginxConfiguratorTest(util.NginxTest):
prefer_ssl=False, prefer_ssl=False,
no_ssl_filter_port='80') no_ssl_filter_port='80')
# Check that the dialog was called with only port 80 vhosts # Check that the dialog was called with only port 80 vhosts
assert len(mock_select_vhs.call_args[0][0]) == 8 assert len(mock_select_vhs.call_args[0][0]) == 9
def test_choose_auth_vhosts(self): def test_choose_auth_vhosts(self):
"""choose_auth_vhosts correctly selects duplicative and HTTP/HTTPS vhosts""" """choose_auth_vhosts correctly selects duplicative and HTTP/HTTPS vhosts"""
@@ -62,7 +62,8 @@ class NginxParserTest(util.NginxTest):
'sites-enabled/globalssl.com', 'sites-enabled/globalssl.com',
'sites-enabled/ipv6.com', 'sites-enabled/ipv6.com',
'sites-enabled/ipv6ssl.com', 'sites-enabled/ipv6ssl.com',
'sites-enabled/example.net']} == \ 'sites-enabled/example.net',
'sites-enabled/addr-80.com']} == \
set(nparser.parsed.keys()) set(nparser.parsed.keys())
assert [['server_name', 'somename', 'alias', 'another.alias']] == \ assert [['server_name', 'somename', 'alias', 'another.alias']] == \
nparser.parsed[nparser.abs_path('server.conf')] nparser.parsed[nparser.abs_path('server.conf')]
@@ -92,7 +93,7 @@ class NginxParserTest(util.NginxTest):
parsed = nparser._parse_files(nparser.abs_path( parsed = nparser._parse_files(nparser.abs_path(
'sites-enabled/example.com.test')) 'sites-enabled/example.com.test'))
assert 4 == len(glob.glob(nparser.abs_path('*.test'))) assert 4 == len(glob.glob(nparser.abs_path('*.test')))
assert 10 == len( assert 11 == len(
glob.glob(nparser.abs_path('sites-enabled/*.test'))) glob.glob(nparser.abs_path('sites-enabled/*.test')))
assert [[['server'], [['listen', '69.50.225.155:9000'], assert [[['server'], [['listen', '69.50.225.155:9000'],
['listen', '127.0.0.1'], ['listen', '127.0.0.1'],
@@ -175,7 +176,7 @@ class NginxParserTest(util.NginxTest):
'*.www.example.com'}, '*.www.example.com'},
[], [2, 1, 0]) [], [2, 1, 0])
assert 19 == len(vhosts) assert 20 == len(vhosts)
example_com = [x for x in vhosts if 'example.com' in x.filep][0] example_com = [x for x in vhosts if 'example.com' in x.filep][0]
assert vhost3 == example_com assert vhost3 == example_com
default = [x for x in vhosts if 'default' in x.filep][0] default = [x for x in vhosts if 'default' in x.filep][0]
@@ -0,0 +1,5 @@
server {
listen 1.2.3.4:80;
listen [1:20::300]:80;
server_name addr-80.com;
}
+2
View File
@@ -26,6 +26,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
* Honor --reuse-key when --allow-subset-of-names is set * Honor --reuse-key when --allow-subset-of-names is set
* Fixed regression in symlink parsing on Windows that was introduced in Certbot * Fixed regression in symlink parsing on Windows that was introduced in Certbot
3.1.0. 3.1.0.
* When adding ssl listen directives in nginx server blocks, IP addresses are now
preserved.
More details about these changes can be found on our GitHub repo. More details about these changes can be found on our GitHub repo.