Wire up PIN authentication for remote installation
Integrate the login page, auth config, and packaging needed for PIN-based remote access. Cockpit's CustomLoginPage only loads login.html and po.js from the package directory; all other static assets must be served from /usr/share/cockpit/static/, so the built login.js and login.css are symlinked there as a separate entrypoint. The conf.d drop-in (50-remote-auth.conf) is shipped to a staging path and only activated at runtime by the anaconda backend when inst.webui.remote is set with a PIN (rhinstaller/anaconda#7228). The coproc wrapper now conditionally passes --local-session=- only when auth is disabled, allowing cockpit-ws to delegate authentication to the PIN handler when auth is active. Resolves: INSTALLER-3373
This commit is contained in:
parent
d7d5238950
commit
66c31db454
8 changed files with 121 additions and 9 deletions
6
Makefile
6
Makefile
|
|
@ -128,11 +128,17 @@ install: $(DIST_TEST) po/LINGUAS
|
|||
cp browser-ext $(DESTDIR)/usr/libexec/anaconda
|
||||
cp gnome-control-center-ext $(DESTDIR)/usr/libexec/anaconda
|
||||
cp src/scripts/cockpit-coproc-wrapper.sh $(DESTDIR)/usr/libexec/anaconda/
|
||||
cp src/scripts/cockpit-pin-auth $(DESTDIR)/usr/libexec/anaconda/
|
||||
cp src/scripts/anaconda-cockpit-conf-merge $(DESTDIR)/usr/libexec/anaconda/
|
||||
mkdir -p $(DESTDIR)/usr/lib/systemd/system/
|
||||
cp src/systemd/webui-cockpit-ws.service $(DESTDIR)/usr/lib/systemd/system/
|
||||
cp src/systemd/cockpit-pin-auth@.service $(DESTDIR)/usr/lib/systemd/system/
|
||||
cp src/systemd/cockpit-pin-auth.socket $(DESTDIR)/usr/lib/systemd/system/
|
||||
mkdir -p $(DESTDIR)/etc/anaconda/cockpit/conf.d/
|
||||
cp src/config/cockpit/cockpit.conf $(DESTDIR)/etc/anaconda/cockpit/cockpit.conf
|
||||
# Staged config, not active by default — webui-desktop copies it to conf.d/ at runtime
|
||||
mkdir -p $(DESTDIR)/usr/share/anaconda/cockpit/conf.d/
|
||||
cp src/config/cockpit/conf.d/50-remote-auth.conf $(DESTDIR)/usr/share/anaconda/cockpit/conf.d/
|
||||
|
||||
dist: $(TARFILE)
|
||||
@ls -1 $(TARFILE)
|
||||
|
|
|
|||
7
build.js
7
build.js
|
|
@ -49,7 +49,7 @@ function watchDirs (dir, onChange) {
|
|||
|
||||
const context = await esbuild.context({
|
||||
bundle: true,
|
||||
entryPoints: ["./src/index.js"],
|
||||
entryPoints: ["./src/index.js", "./src/anacondaLogin.js"],
|
||||
external: ["*.woff", "*.woff2", "*.jpg", "*.svg", "../../assets*"],
|
||||
legalComments: "external", // Move all legal comments to a .LEGAL.txt file
|
||||
loader: {
|
||||
|
|
@ -69,6 +69,7 @@ const context = await esbuild.context({
|
|||
{ from: ["./images/qr-code-feedback.svg"], to: ["./qr-code-feedback.svg"] },
|
||||
{ from: ["./src/manifest.json"], to: ["./manifest.json"] },
|
||||
{ from: ["./src/index.html"], to: ["./index.html"] },
|
||||
{ from: ["./src/login.html"], to: ["./login.html"] },
|
||||
{ from: ["./VERSION.txt"], to: ["./VERSION.txt"] },
|
||||
]
|
||||
}),
|
||||
|
|
@ -77,7 +78,9 @@ const context = await esbuild.context({
|
|||
quietDeps: true,
|
||||
}),
|
||||
cockpitPoEsbuildPlugin(),
|
||||
cockpitCompressPlugin(),
|
||||
// Login assets are served via /cockpit/static/ which does not
|
||||
// support .gz transparent serving, causing MIME type mismatch.
|
||||
cockpitCompressPlugin({ exclude: /anacondaLogin\.(js|css)$/ }),
|
||||
cockpitRsyncEsbuildPlugin({ dest: packageJson.name }),
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,7 +65,13 @@ exit 0
|
|||
%files
|
||||
%dir %{_datadir}/cockpit/anaconda-webui
|
||||
%doc README.rst
|
||||
%license LICENSE dist/index.js.LEGAL.txt
|
||||
%license LICENSE dist/index.js.LEGAL.txt dist/anacondaLogin.js.LEGAL.txt
|
||||
%{_datadir}/cockpit/anaconda-webui/login.html
|
||||
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js
|
||||
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js.map
|
||||
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js.LEGAL.txt
|
||||
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.css
|
||||
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.css.map
|
||||
%{_datadir}/cockpit/anaconda-webui/qr-code-feedback.svg
|
||||
%{_datadir}/cockpit/anaconda-webui/index.js.LEGAL.txt
|
||||
%{_datadir}/cockpit/anaconda-webui/index.html
|
||||
|
|
@ -81,6 +87,10 @@ exit 0
|
|||
%dir /etc/anaconda/cockpit
|
||||
%config(noreplace) /etc/anaconda/cockpit/cockpit.conf
|
||||
%dir /etc/anaconda/cockpit/conf.d
|
||||
%dir %{_datadir}/anaconda/cockpit
|
||||
%dir %{_datadir}/anaconda/cockpit/conf.d
|
||||
%{_datadir}/anaconda/cockpit/conf.d/50-remote-auth.conf
|
||||
%{_libexecdir}/anaconda/cockpit-pin-auth
|
||||
%dir %{_datadir}/anaconda/firefox-theme
|
||||
%dir %{_datadir}/anaconda/firefox-theme/default
|
||||
%dir %{_datadir}/anaconda/firefox-theme/default/chrome
|
||||
|
|
@ -98,6 +108,8 @@ exit 0
|
|||
%{_datadir}/applications/extlinks.desktop
|
||||
%{_datadir}/applications/anaconda-gnome-control-center.desktop
|
||||
%{_unitdir}/webui-cockpit-ws.service
|
||||
%{_unitdir}/cockpit-pin-auth.socket
|
||||
%{_unitdir}/cockpit-pin-auth@.service
|
||||
|
||||
|
||||
# The changelog is automatically generated and merged
|
||||
|
|
|
|||
15
src/config/cockpit/conf.d/50-remote-auth.conf
Normal file
15
src/config/cockpit/conf.d/50-remote-auth.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright (C) 2026 Red Hat, Inc.
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Cockpit PIN authentication for remote WebUI access.
|
||||
# This file is NOT active by default. It is shipped to
|
||||
# /usr/share/anaconda/cockpit/conf.d/ and only copied into the
|
||||
# active /etc/anaconda/cockpit/conf.d/ at runtime by webui-desktop
|
||||
# when inst.webui.remote is set with a password and
|
||||
# inst.webui.remote.noauth is not set.
|
||||
|
||||
[WebService]
|
||||
CustomLoginPage = /usr/share/cockpit/anaconda-webui/
|
||||
|
||||
[Basic]
|
||||
Command = /usr/libexec/anaconda/cockpit-pin-auth
|
||||
|
|
@ -12,6 +12,11 @@ WEBUI_ADDRESS=$1
|
|||
# Start cockpit-bridge in unconfined context via su using coproc
|
||||
coproc BRIDGE { cockpit-bridge; }
|
||||
|
||||
# Start cockpit-ws connected to the coproc
|
||||
# TLS settings are now in /etc/anaconda/cockpit/cockpit.conf (AllowUnencrypted)
|
||||
exec /usr/libexec/cockpit-ws -p 80 -a "$WEBUI_ADDRESS" --local-session=- <&${BRIDGE[0]} >&${BRIDGE[1]}
|
||||
# When auth is not enabled, use --local-session=- to connect
|
||||
# directly to the bridge (skips cockpit authentication).
|
||||
WS_EXTRA_ARGS=()
|
||||
if [[ "${WEBUI_AUTH:-0}" != "1" ]]; then
|
||||
WS_EXTRA_ARGS+=(--local-session=-)
|
||||
fi
|
||||
|
||||
exec /usr/libexec/cockpit-ws "${WS_EXTRA_ARGS[@]}" -p 80 -a "$WEBUI_ADDRESS" <&${BRIDGE[0]} >&${BRIDGE[1]}
|
||||
|
|
|
|||
53
test/check-remote-auth
Executable file
53
test/check-remote-auth
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/bin/python3
|
||||
#
|
||||
# Copyright (C) 2026 Red Hat, Inc.
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
from anacondalib import INSTALLER_VM_MEMORY_MB, VirtInstallMachineCase
|
||||
from installer import Installer
|
||||
from testlib import test_main # pylint: disable=import-error
|
||||
|
||||
|
||||
class TestRemoteAuth(VirtInstallMachineCase):
|
||||
provision = {
|
||||
"machine1": {
|
||||
"remote_pin": "test1234",
|
||||
"memory_mb": INSTALLER_VM_MEMORY_MB,
|
||||
},
|
||||
}
|
||||
|
||||
def testBasic(self):
|
||||
"""
|
||||
Description:
|
||||
Boot with ``inst.webui.remote inst.webui.remote.password=test1234``
|
||||
and verify that PIN authentication is required.
|
||||
|
||||
Expected results:
|
||||
- The login page is shown
|
||||
- Wrong PIN is rejected with an error
|
||||
- Correct PIN grants access to the installer
|
||||
"""
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
_i = Installer(b, m)
|
||||
|
||||
b.open("/cockpit/@localhost/anaconda-webui/index.html")
|
||||
|
||||
# Login page should be visible
|
||||
b.wait_visible("#pin-input")
|
||||
|
||||
# Wrong password should be rejected
|
||||
b.set_input_text("#pin-input", "wrongpin")
|
||||
b.click("button[type=submit]")
|
||||
b.wait_in_text(".pf-v6-c-alert", "Invalid PIN")
|
||||
|
||||
# Correct password should grant access
|
||||
b.set_input_text("#pin-input", "test1234")
|
||||
b.click("button[type=submit]")
|
||||
|
||||
# After login redirect, the installer loads at the root path
|
||||
b.wait_js_cond('window.location.pathname === "/cockpit/@localhost/anaconda-webui/index.html"')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main()
|
||||
|
|
@ -45,6 +45,7 @@ class VirtInstallMachine(VirtMachine):
|
|||
self.kickstart_file_name = kwargs.pop("kickstart_file_name", None)
|
||||
self.pause_at_summary = kwargs.pop("pause_at_summary", False)
|
||||
self.payload_type = kwargs.pop("payload_type", "liveimg".lower())
|
||||
self.remote_pin = kwargs.pop("remote_pin", "")
|
||||
# Always enforce the minimum RAM the installer requires. Covers every entry point the same way:
|
||||
# test/run (CI), local test/check-*, and GlobalMachine.reset() which omits memory_mb.
|
||||
kwargs["memory_mb"] = max(kwargs.get("memory_mb") or 0, INSTALLER_VM_MEMORY_MB)
|
||||
|
|
@ -194,6 +195,7 @@ class VirtInstallMachine(VirtMachine):
|
|||
if self.console_file
|
||||
else ""
|
||||
)
|
||||
auth_method = f"inst.webui.remote.pin={self.remote_pin}" if self.remote_pin else "inst.webui.remote.noauth"
|
||||
try:
|
||||
self._execute(
|
||||
"virt-install "
|
||||
|
|
@ -208,7 +210,7 @@ class VirtInstallMachine(VirtMachine):
|
|||
f"{serial_opt}"
|
||||
f"--graphics vnc,listen={self.ssh_address} "
|
||||
"--extra-args "
|
||||
f"'{virt_kargs}{inst_graphical}inst.sshd inst.webui inst.webui.remote {selinux}{inst_ks_arg} "
|
||||
f"'{virt_kargs}{inst_graphical}inst.sshd inst.webui inst.webui.remote {auth_method} {selinux}{inst_ks_arg} "
|
||||
f"inst.updates=http://10.0.2.2:{self.http_install_port}/"
|
||||
f"{self.label}-updates.img' "
|
||||
"--network none "
|
||||
|
|
|
|||
|
|
@ -34,13 +34,15 @@ BROWSER_HOME=""
|
|||
|
||||
THEME_ID="default"
|
||||
WEBUI_REMOTE=0
|
||||
while getopts t:r: option
|
||||
WEBUI_NOAUTH=0
|
||||
while getopts t:r:n: option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
t)THEME_ID=${OPTARG};;
|
||||
r)WEBUI_REMOTE=${OPTARG};;
|
||||
*) echo "Usage: $0 [-t THEME_ID] [-r WEBUI_REMOTE]" >&2
|
||||
n)WEBUI_NOAUTH=${OPTARG};;
|
||||
*) echo "Usage: $0 [-t THEME_ID] [-r WEBUI_REMOTE] [-n WEBUI_NOAUTH]" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -53,7 +55,21 @@ then
|
|||
WEBUI_ADDRESS="0.0.0.0"
|
||||
fi
|
||||
|
||||
REMOTE_AUTH_DROPIN="/etc/anaconda/cockpit/conf.d/50-remote-auth.conf"
|
||||
REMOTE_AUTH_SOURCE="/usr/share/anaconda/cockpit/conf.d/50-remote-auth.conf"
|
||||
PIN_FILE="/run/anaconda/remote-pin"
|
||||
WEBUI_AUTH=0
|
||||
|
||||
if [[ -f "$PIN_FILE" ]]; then
|
||||
cp "$REMOTE_AUTH_SOURCE" "$REMOTE_AUTH_DROPIN"
|
||||
WEBUI_AUTH=1
|
||||
else
|
||||
rm -f "$REMOTE_AUTH_DROPIN"
|
||||
fi
|
||||
|
||||
echo "WEBUI_ADDRESS=$WEBUI_ADDRESS" > /tmp/webui-cockpit-ws.env
|
||||
echo "WEBUI_AUTH=$WEBUI_AUTH" >> /tmp/webui-cockpit-ws.env
|
||||
|
||||
systemctl start webui-cockpit-ws
|
||||
|
||||
# Function to setup user-related environment
|
||||
|
|
|
|||
Loading…
Reference in a new issue