Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

namespace OPNsense\AcmeClient\LeValidation;

require_once('util.inc');

use OPNsense\AcmeClient\LeValidationInterface;
use OPNsense\AcmeClient\LeUtils;
use OPNsense\Core\Config;
Expand Down Expand Up @@ -77,21 +79,38 @@ public function prepare()
$backend = new \OPNsense\Core\Backend();
$interface = (string)$this->config->http_opn_interface;
$response = json_decode($backend->configdpRun('interface address', [$interface]));
// XXX Returns both IPv4 and IPv6 now. While "[0]" and
// "[1]" should remain in this order it would make sense
// to ensure "family" matches "inet" or "inet6" and/or
// pull both addresses for missing IPv6 support depending
// on how this should work.
if (!empty($response->$interface[0]->address)) {
$iplist[] = $response->$interface[0]->address;
}
foreach ($response->$interface as $if) {
if (!empty($if->address)) {
$iplist[] = $if->address;
}
}
}

// Check if IPv6 support is enabled
if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == '1')) {
$_ipv6_enabled = true;
} else {
$_ipv6_enabled = false;
// Find redirect target for IPv6
//
// Needed because redirecting to ::1 isn't allowed [1].
//
// [1]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193568
$ipv6_redirect_addr = null;
if (is_ipv6_allowed()) {
$backend = new \OPNsense\Core\Backend();
$interface = "wan";
$response = json_decode($backend->configdpRun('interface address', [$interface]));

if (isset($response->$interface)) {
foreach ($response->$interface as $if) {
if (!empty($if->address) && $if->family == "inet6") {
$ipv6_redirect_addr = $if->address;
break;
}
}
}

if ($ipv6_redirect_addr != null) {
LeUtils::log("found IPv6 on WAN interface, will redirect traffic there ({$ipv6_redirect_addr})");
} else {
LeUtils::log("failed to find IPv6 on WAN interface ($interface), will not rewrite target address");
}
}

// Generate rules for all IP addresses
Expand All @@ -105,9 +124,9 @@ public function prepare()
$_dst = '127.0.0.1';
$_family = 'inet';
LeUtils::log("using IPv4 address: {$ip}");
} elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
} elseif (is_ipv6_allowed() && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
// IPv6
$_dst = '::1';
$_dst = $ipv6_redirect_addr != null ? $ipv6_redirect_addr : $ip;
$_family = 'inet6';
LeUtils::log("using IPv6 address: {$ip}");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

namespace OPNsense\AcmeClient\LeValidation;

require_once('util.inc');

use OPNsense\AcmeClient\LeValidationInterface;
use OPNsense\AcmeClient\LeUtils;
use OPNsense\Core\Config;
Expand Down Expand Up @@ -78,21 +80,38 @@ public function prepare()
$backend = new \OPNsense\Core\Backend();
$interface = (string)$this->config->tlsalpn_acme_interface;
$response = json_decode($backend->configdpRun('interface address', [$interface]));
// XXX Returns both IPv4 and IPv6 now. While "[0]" and
// "[1]" should remain in this order it would make sense
// to ensure "family" matches "inet" or "inet6" and/or
// pull both addresses for missing IPv6 support depending
// on how this should work.
if (!empty($response->$interface[0]->address)) {
$iplist[] = $response->$interface[0]->address;
}
foreach ($response->$interface as $if) {
if (!empty($if->address)) {
$iplist[] = $if->address;
}
}
}

// Check if IPv6 support is enabled
if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == '1')) {
$_ipv6_enabled = true;
} else {
$_ipv6_enabled = false;
// Find redirect target for IPv6
//
// Needed because redirecting to ::1 isn't allowed [1].
//
// [1]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=193568
$ipv6_redirect_addr = null;
if (is_ipv6_allowed()) {
$backend = new \OPNsense\Core\Backend();
$interface = "wan";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you hardcoding wan here? The interface is specified in $this->config->tlsalpn_acme_interface. It's not always wan, the IP might be configured on a different interface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirecting to ::1 isn't possible, as described in the commit message. This is simply an attempt find any address in the right scope. Any address really, on any interface, that belongs to this host. I could use tlsalpn_acme_interface but, to my knowledge, there is no guarantee that this interface is set.

Copy link
Copy Markdown
Member

@fraenki fraenki Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use $this->config->tlsalpn_acme_interface. This way it remains configurable. Hard-coding a specific interface name is not going to work. You may log an error if tlsalpn_acme_interface is empty.

And also update the "found IPv6 on WAN interface" log messages accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $this->config->tlsalpn_acme_interface is set, it likely has a suitable (=in the right scope) IPv6 address, which is good. How do you feel about checking if $this->config->tlsalpn_acme_interface is set, and if not, as last resort, just try wan. I do feel like a lot of setups have wan and we don't lose anything by trying that interface.

Again, the right solution here is probably to go through all interfaces and find suitable IPv6 address but I feel like that will take me too much time to figure out and this is a solution which should already work for the majority of cases. We can always improve this later, if I or someone else finds time.

$response = json_decode($backend->configdpRun('interface address', [$interface]));

if (isset($response->$interface)) {
foreach ($response->$interface as $if) {
if (!empty($if->address) && $if->family == "inet6") {
$ipv6_redirect_addr = $if->address;
break;
}
}
}

if ($ipv6_redirect_addr != null) {
LeUtils::log("found IPv6 on WAN interface, will redirect traffic there ({$ipv6_redirect_addr})");
} else {
LeUtils::log("failed to find IPv6 on WAN interface ($interface), will not rewrite target address");
}
}

// Generate rules for all IP addresses
Expand All @@ -106,9 +125,9 @@ public function prepare()
$_dst = '127.0.0.1';
$_family = 'inet';
LeUtils::log("using IPv4 address: {$ip}");
} elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
} elseif (is_ipv6_allowed() && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
// IPv6
$_dst = '::1';
$_dst = $ipv6_redirect_addr != null ? $ipv6_redirect_addr : $ip;
$_family = 'inet6';
LeUtils::log("using IPv6 address: {$ip}");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# FreeBSD!
server.event-handler = "freebsd-kqueue"
server.network-backend = "writev"
#server.use-ipv6 = "enable"
server.use-ipv6 = "{{ 'enable' if OPNsense.Interfaces.settings.disableipv6 == "0" else 'disable' }}"

# modules to load
server.modules = ( "mod_access", "mod_expire", "mod_deflate", "mod_redirect",
Expand Down Expand Up @@ -60,13 +60,10 @@ mimetype.assign = (
url.access-deny = ( "~", ".inc" )

# bind to port
server.bind = "127.0.0.1"
server.port = {{OPNsense.AcmeClient.settings.challengePort}}
$SERVER["socket"] == "127.0.0.1:{{OPNsense.AcmeClient.settings.challengePort}}" { }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you removing this? It's there for a reason.

Copy link
Copy Markdown
Contributor Author

@pgerber pgerber Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that this was added to have the web server only bind to localhost (::1 and 127.0.0.1). However, as stated in the commit message, we cannot do this for IPv6 because ::1 is a scope that doesn't allow traffic coming from another interface. Removing this reverts binding to the default behavior, namely, binding to all interfaces.

I could try to only bind to specific addresses but this seems a bit pointless. It would require to generate this config for every Challenge Type. Addresses my differ between them. I also looked at some other services running on OPNsense and binding to all interfaces and then using the firewall to restrict access seems the norm:

# netstat -an -f inet6
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address          Foreign Address        (state)    
...
tcp6       0      0 *.53                   *.*                    LISTEN     
tcp6       0      0 *.53                   *.*                    LISTEN     
tcp6       0      0 *.53                   *.*                    LISTEN     
tcp6       0      0 *.53                   *.*                    LISTEN     
tcp6       0      0 *.22                   *.*                    LISTEN     
tcp6       0      0 *.443                  *.*                    LISTEN     
tcp6       0      0 *.43580                *.*                    LISTEN     
...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually found some issues with this while testing with IPv6 disabled and I ended up readding explicit binding for IPv4 and IPv6.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this reverts binding to the default behavior, namely, binding to all interfaces.

This would represent a fundamental change in behavior. Initially, the Acme web service was designed to run only on localhost and to be exposed solely via (temporary) port redirection or through internal HTTP requests (e.g. via HAProxy).

You are now proposing to run this service on all interfaces, which could potentially conflict with other services running on the OPNsense system and might accidentally (and permanently) expose this service to the public.

I strongly oppose this change in behavior. Please ensure that the service binds only to 127.0.0.1 for IPv4 and propose a solution for IPv6.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to respond here. Redirecting to ::1 is obviously not a possibility, it does not work. So, the one alternative here I can think of is to officially not support IPv6, at least not when using the ACME web service.

The web server running is running on dedicated port and traffic is redirected there. I find it unlikely that it would conflict with some other service listening on a different IP/interface that just happens to use the same (probably random) port. Even if, such a service would likely conflict anyway, because it would want to listen to localhost too.

Arguably, it's easier to expose the service by accident, as you mentioned. However, all critical services are listening on all interfaces, including the web interface and SSH. If you don't take care of restricting access properly, you probably have bigger problems than this web server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirecting to ::1 is obviously not a possibility, it does not work. So, the one alternative here I can think of is to officially not support IPv6, at least not when using the ACME web service.

Why not introduce a new field and let the user specify the IPv6 address? Or use the IPv6 address of the selected interface?

I find it unlikely that it would conflict with some other service listening on a different IP/interface that just happens to use the same (probably random) port. Even if, such a service would likely conflict anyway, because it would want to listen to localhost too.

I disagree. You just cannot know this and there are ways to circumvent this potential issue.

However, all critical services are listening on all interfaces, including the web interface and SSH. If you don't take care of restricting access properly, you probably have bigger problems than this web server.

We are talking about changing the behaviour of an existing service that was supposedly already secured by the user/admin. So that's definitely something we must avoid. It's violating POLA.

Please let's just move on from this discussion and let's work on a solution.


{% if helpers.exists('system.ipv6allow') and system.ipv6allow|default("0") == "1" %}
# IPv6
$SERVER["socket"] == "[::1]:{{OPNsense.AcmeClient.settings.challengePort}}" { }
$SERVER["socket"] == "0.0.0.0:{{OPNsense.AcmeClient.settings.challengePort}}" { }
{% if OPNsense.Interfaces.settings.disableipv6 == "1" %}
$SERVER["socket"] == "[::]:{{OPNsense.AcmeClient.settings.challengePort}}" { }
{% endif %}

# to help the rc.scripts
Expand Down