Skip to content

Commit 078af95

Browse files
lslezakmvidner
andauthored
Optionally disable remote access (#3269)
## Problem - https://jira.suse.com/browse/AGM-153 - For security reasons it should be possible to disable remote access to the Agama web server. A server which is not reachable cannot be hacked. :smiley: ## Solution - Add a new `inst.listen_on` boot option, the possible values: - `inst.listen_on=all` - listen on all network interfaces (allow local and remote access). This is the default behavior used even without the `inst.listen_on` option, added just for completeness. - `inst.listen_on=localhost` - listen only on loop back (localhost) device. This disables remote access, Agama can be accessed only locally. - `inst.listen_on=<ip>` - listen on the specified IP address. Both IPv4 and IPv6 addresses are supported. It is possible to use multiple IP addresses separated by comma. Addresses not found in the system are ignored. - `inst.listen_on=<interface>` - listen on the specified network interface. Multiple interfaces can be separated by comma. Not found interfaces are ignored. Agama always listens on the local loop back interface even when specifying a specific network interface or IP address for listening. The reason is to avoid reporting connection errors by the Firefox started in the Live ISO. ## Details - The `--address2` CLI option has been removed, instead it is possible to specify `--address` option multiple times. - The PR includes the @mvidner's patch #3111 - fallback to an IPv4 address when listening to IPv6 address fails (when IPv6 is disabled with the `ipv6.disable=1` boot option) - Added the `agama-web-server.sh` wrapper script started from the systemd service. It evaluates the boot parameters and builds the address parameters for the Agama server. ## Notes - The other network services like SSH can be disabled using the standard `systemd.mask` boot option. For example to disable the SSH service use this boot option: `systemd.mask=sshd.service`. (I'll document this as well...) ## Testing - Tested manually in all scenarios: with disabled remote access, listening on the specified IPv6 (including link local address) or IPv4 address, listening on specified interface, listening on multiple interfaces - Tested Martin's patch with the `ipv6.disable=1` boot option, Agama properly listens on the IPv4 addresses in that case. --------- Co-authored-by: Martin Vidner <mvidner@suse.com>
1 parent ce27186 commit 078af95

File tree

11 files changed

+141
-33
lines changed

11 files changed

+141
-33
lines changed

rust/WEB-SERVER.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ Some more examples:
6868
- IPv4, only specific interface: `--address 192.168.1.2:5678` (use the IP
6969
address of that interface)
7070

71-
The server can optionally listen on a secondary address, use the `--address2`
72-
option for that.
71+
The server can listen on several addresses, you can use the `--address` option
72+
multiple times.
7373

7474
## Trying the server
7575

rust/agama-server/src/agama-web-server.rs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,9 @@ struct Cli {
7979
struct ServeArgs {
8080
// Address/port to listen on. ":::80" listens for both IPv6 and IPv4
8181
// connections unless manually disabled in /proc/sys/net/ipv6/bindv6only.
82-
/// Primary port to listen on
82+
/// Address:port to listen to, with comma-separated fallback address:port; can be repeated
8383
#[arg(long, default_value = ":::80")]
84-
address: String,
85-
86-
/// Optional secondary address to listen on
87-
#[arg(long, default_value = None)]
88-
address2: Option<String>,
84+
address: Vec<String>,
8985

9086
#[arg(long, default_value = "/etc/agama.d/ssl/key.pem")]
9187
key: Option<PathBuf>,
@@ -273,19 +269,28 @@ async fn handle_http_stream(
273269
}
274270
}
275271

272+
async fn find_listener(addresses: String) -> Option<tokio::net::TcpListener> {
273+
let addresses = addresses.split(',').collect::<Vec<_>>();
274+
for addr in addresses {
275+
tracing::info!("Starting Agama web server at {}", addr);
276+
// see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs
277+
// how to use axum with openSSL
278+
match tokio::net::TcpListener::bind(&addr).await {
279+
Ok(listener) => {
280+
return Some(listener);
281+
}
282+
Err(error) => {
283+
tracing::warn!("Error: could not listen on {}: {}", &addr, error);
284+
}
285+
}
286+
}
287+
None
288+
}
289+
276290
/// Starts the web server
277291
async fn start_server(address: String, service: Router, ssl_acceptor: SslAcceptor) {
278-
tracing::info!("Starting Agama web server at {}", address);
279-
280-
// see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs
281-
// how to use axum with openSSL
282-
let listener = tokio::net::TcpListener::bind(&address)
283-
.await
284-
.unwrap_or_else(|error| {
285-
let msg = format!("Error: could not listen on {}: {}", &address, error);
286-
tracing::error!(msg);
287-
panic!("{}", msg)
288-
});
292+
let opt_listener = find_listener(address).await;
293+
let listener = opt_listener.expect("None of the alternative addresses worked");
289294

290295
pin_mut!(listener);
291296

@@ -300,7 +305,7 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto
300305
let (tcp_stream, addr) = listener
301306
.accept()
302307
.await
303-
.expect("Failed to open port for listening");
308+
.expect("Failed to accept connection");
304309

305310
tokio::spawn(async move {
306311
if is_ssl_stream(&tcp_stream).await {
@@ -341,13 +346,8 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> {
341346
return Err(anyhow::anyhow!("SSL initialization failed"));
342347
};
343348

344-
let mut addresses = vec![args.address];
345-
346-
if let Some(a) = args.address2 {
347-
addresses.push(a)
348-
}
349-
350-
let servers: Vec<_> = addresses
349+
let servers: Vec<_> = args
350+
.address
351351
.iter()
352352
.map(|a| {
353353
tokio::spawn(start_server(

rust/install.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama"
3838
install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-autoinstall"
3939
install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-proxy-setup"
4040
install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-web-server"
41+
install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/share/agama-web-server.sh"
4142

4243
install6 -D -p "${SRCDIR}"/share/agama.pam "${DESTDIR}${pamvendordir}"/agama
4344

rust/package/agama.changes

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
-------------------------------------------------------------------
2+
Thu Mar 12 13:05:51 UTC 2026 - Ladislav Slezák <lslezak@suse.com>
3+
4+
- Added support for the "inst.listen_on" boot parameter
5+
- The "inst.listen_on=localhost" option disables remote access
6+
- It is possible to specify IP address (both IPv4 and IPv6)
7+
or network interface, accepts comma separated list
8+
- agama-web-server --address2 option removed, --address can now be repeated
9+
(jsc#AGM-153)
10+
111
-------------------------------------------------------------------
212
Thu Mar 12 10:03:39 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>
313

rust/package/agama.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ echo $PATH
245245
%doc README.md
246246
%license LICENSE
247247
%{_bindir}/agama-web-server
248+
%{_bindir}/agama-web-server.sh
248249
%{_bindir}/agama-proxy-setup
249250
%{_pam_vendordir}/agama
250251
%{_unitdir}/agama-web-server.service

rust/share/agama-web-server.service

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ BindsTo=agama.service
99
EnvironmentFile=-/run/agama/environment.conf
1010
Environment="AGAMA_LOG=debug,zbus=info"
1111
Type=notify
12-
ExecStart=/usr/bin/agama-web-server serve --address :::80 --address2 :::443
12+
ExecStart=/usr/bin/agama-web-server.sh
1313
PIDFile=/run/agama/web.pid
1414
User=root
1515
TimeoutStopSec=5

rust/share/agama-web-server.sh

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/bash
2+
#
3+
# Copyright (c) [2026] SUSE LLC
4+
#
5+
# All Rights Reserved.
6+
#
7+
# This program is free software; you can redistribute it and/or modify it
8+
# under the terms of the GNU General Public License as published by the Free
9+
# Software Foundation; either version 2 of the License, or (at your option)
10+
# any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful, but WITHOUT
13+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15+
# more details.
16+
#
17+
# You should have received a copy of the GNU General Public License along
18+
# with this program; if not, contact SUSE LLC.
19+
#
20+
# To contact SUSE LLC about this file by physical or electronic mail, you may
21+
# find current contact information at www.suse.com.
22+
23+
# This script is a wrapper for the Agama web server, it evaluates to which
24+
# addresses the server should listen to.
25+
26+
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
27+
echo "Usage: $0"
28+
echo
29+
echo " This is a wrapper script for the Agama web server (agama-web-server)."
30+
echo
31+
echo " It configures the listening addresses for the web server based on"
32+
echo " the \"inst.listen_on\" boot option."
33+
exit 0
34+
fi
35+
36+
# the default options: listen on all interfaces for both HTTP and HTTPS ports,
37+
# the IPv4 addresses are fallbacks when IPv6 is disabled with the
38+
# "ipv6.disable=1" kernel boot option
39+
DEFAULT_OPTIONS=(--address ":::80,0.0.0.0:80" --address ":::443,0.0.0.0:443")
40+
# options for localhost access only
41+
LOCAL_OPTIONS=(--address "::1:80,127.0.0.1:80" --address "::1:443,127.0.0.1:443")
42+
43+
# check if the "inst.listen_on=" boot option was used
44+
if grep -q "\binst.listen_on=.\+" /run/agama/cmdline.d/agama.conf; then
45+
LISTEN_ON=$(grep "\binst.listen_on=.\+" /run/agama/cmdline.d/agama.conf | sed 's/.*\binst.listen_on=\([^[:space:]]\+\)/\1/')
46+
47+
if [ "$LISTEN_ON" = "localhost" ]; then
48+
OPTIONS=("${LOCAL_OPTIONS[@]}")
49+
elif [ "$LISTEN_ON" = "all" ]; then
50+
OPTIONS=("${DEFAULT_OPTIONS[@]}")
51+
else
52+
# always run on the localhost
53+
OPTIONS=("${LOCAL_OPTIONS[@]}")
54+
55+
# the string can contain multiple addresses separated by comma,
56+
# replace commas with spaces and iterate over items
57+
ADDRESSES=${LISTEN_ON//,/ }
58+
for ADDRESS in $ADDRESSES; do
59+
# check if the value is an IP address (IPv6, IPv6 link local or IPv4)
60+
if echo "$ADDRESS" | grep -qE '^[0-9a-fA-F:]+$|^[fF][eE]80|^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
61+
echo "<5>Listening on IP address ${ADDRESS}"
62+
OPTIONS+=(--address "${ADDRESS}:80" --address "${ADDRESS}:443")
63+
else
64+
# otherwise assume it is as an interface name
65+
if ip addr show dev "${ADDRESS}" >/dev/null 2>&1; then
66+
# find the IP address for the specified interface
67+
IP_ADDRS=$(ip -o addr show dev "${ADDRESS}" | awk '{print $4}' | cut -d/ -f1)
68+
if [ -n "${IP_ADDRS}" ]; then
69+
for IP in $IP_ADDRS; do
70+
# append the %device for link local IPv6 addresses
71+
if [[ "$IP" == fe80* ]]; then
72+
IP="${IP}%${ADDRESS}"
73+
fi
74+
echo "<5>Listening on interface ${ADDRESS} with IP address ${IP}"
75+
OPTIONS+=(--address "${IP}:80" --address "${IP}:443")
76+
done
77+
else
78+
echo "<3>IP address for interface ${ADDRESS} not found"
79+
fi
80+
else
81+
echo "<3>Network Interface ${ADDRESS} not found"
82+
fi
83+
fi
84+
done
85+
fi
86+
else
87+
OPTIONS=("${DEFAULT_OPTIONS[@]}")
88+
fi
89+
90+
if [ "${OPTIONS[*]}" = "${DEFAULT_OPTIONS[*]}" ]; then
91+
echo "<5>Listening on all network interfaces"
92+
elif [ "${OPTIONS[*]}" = "${LOCAL_OPTIONS[*]}" ]; then
93+
echo "<5>Disabling remote access to the Agama web server"
94+
fi
95+
96+
echo "<5>Starting Agama web server with options: ${OPTIONS[*]}"
97+
exec /usr/bin/agama-web-server serve "${OPTIONS[@]}"

rust/zypp-agama/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -597,8 +597,7 @@ impl Zypp {
597597
// save the solver testcase if the solver run failed or if saving is forced via boot
598598
// parameter, skip when "ZYPP_FULLLOG=1", in that case libzypp creates the solver
599599
// testcase automatically in the /var/log/YaST2/autoTestcase/ directory
600-
if (!r_res || save_testcase) && std::env::var("ZYPP_FULLLOG").unwrap_or_default() != "1"
601-
{
600+
if (!r_res || save_testcase) && !std::env::var("ZYPP_FULLLOG").is_ok_and(|v| v == "1") {
602601
self.create_solver_testcase();
603602
} else {
604603
// delete the solver testcase directory, it contains the previous error which is

rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ bool is_package_selected(struct Zypp *zypp, const char *tag,
231231
bool run_solver(struct Zypp *zypp, bool only_required,
232232
struct Status *status) noexcept;
233233

234-
/// Create a solver testcase, dumps all all solver data (repositories, loaded
234+
/// Create a solver testcase, dumps all solver data (repositories, loaded
235235
/// packages...) to disk
236236
/// @param zypp see \ref init_target
237237
/// @param dir directory path where the solver testcase is saved

rust/zypp-agama/zypp-agama-sys/src/bindings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ unsafe extern "C" {
674674
) -> bool;
675675
#[doc = " Runs solver\n @param zypp see \\ref init_target\n @param only_required if true, only required packages are installed (ignoring\n recommended packages)\n @param[out] status (will overwrite existing contents)\n @return true if solver pass and false if it found some dependency issues"]
676676
pub fn run_solver(zypp: *mut Zypp, only_required: bool, status: *mut Status) -> bool;
677-
#[doc = " Create a solver testcase, dumps all all solver data (repositories, loaded\n packages...) to disk\n @param zypp see \\ref init_target\n @param dir directory path where the solver testcase is saved\n @return true if the solver testcase was successfully created"]
677+
#[doc = " Create a solver testcase, dumps all solver data (repositories, loaded\n packages...) to disk\n @param zypp see \\ref init_target\n @param dir directory path where the solver testcase is saved\n @return true if the solver testcase was successfully created"]
678678
pub fn create_solver_testcase(zypp: *mut Zypp, dir: *const ::std::os::raw::c_char) -> bool;
679679
#[doc = " the last call that will free all pointers to zypp holded by agama"]
680680
pub fn free_zypp(zypp: *mut Zypp);

0 commit comments

Comments
 (0)