From 29363bcd23e5aa816a13a4c5f9e2df665fa5674b Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 2 Mar 2026 16:04:10 +0100 Subject: [PATCH 1/5] feat: 24 hours validity for support's SSH key Narrow the validity period from 1 month to 24 hours. --- core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl b/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl index fde9d9de0..ab594dc98 100755 --- a/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl +++ b/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl @@ -16,7 +16,7 @@ pubkey_file=${3:-${AGENT_INSTALL_DIR:?}/etc/support/ssh-rsa.pub} function add_ssh_key { local keyopts - keyopts=$(printf 'expiry-time="%s",from="127.0.0.1"' "$(date -d 'next month' +'%Y%m%dZ')") + keyopts=$(printf 'expiry-time="%s",from="127.0.0.1"' "$(date --utc -d '24 hours' +'%Y%m%d%H%M%SZ')") umask 077 printf "%s %s %s\n" "${keyopts}" "$(cat "${pubkey_file}")" "${key_comment}" >> "${authkeys_file}" } From 53a69a7c59c599450058d4c7604a315c79b43f60 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 2 Mar 2026 16:25:40 +0100 Subject: [PATCH 2/5] feat: support.service max session duration - Automatically stop the support.service unit 24 hours after support begins. - The auxiliary unit support-expire.timer can be stopped manually, to extend the support session duration as wanted. - In any case support.service will die after 30 days. --- core/imageroot/etc/systemd/system/support.service | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/imageroot/etc/systemd/system/support.service b/core/imageroot/etc/systemd/system/support.service index 224ec1c63..9f39e587a 100644 --- a/core/imageroot/etc/systemd/system/support.service +++ b/core/imageroot/etc/systemd/system/support.service @@ -24,10 +24,17 @@ ExecStart=runagent -m node podman run \ ${SUPPORT_IMAGE} ExecStartPost=runagent -m node bash -c "nsenter -t $$(podman ps --format='{{.Pid}}' --filter=id=$$(< %t/support.cid)) -n -- nft -f $${AGENT_INSTALL_DIR}/etc/support/nft.conf" ExecStartPost=runagent -m cluster support-clusteradminctl add +ExecStartPost=/usr/bin/systemd-run \ + --collect \ + --unit=%N-expire.timer \ + --on-active=24h \ + /usr/bin/systemctl stop %n ExecStop=/usr/bin/podman stop --ignore --cidfile %t/support.cid -t 10 ExecStopPost=runagent -m node support-sshkeyctl remove ExecStopPost=runagent -m cluster support-clusteradminctl remove ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/support.cid +ExecStopPost=-/usr/bin/systemctl --no-block stop %N-expire.timer PIDFile=%t/support.pid Type=forking SyslogIdentifier=support +RuntimeMaxSec=30d From 739157f33141f70759970bf0169fc10973b987f9 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 2 Mar 2026 17:25:09 +0100 Subject: [PATCH 3/5] feat(get-support-session): expires_at timestamp Return the session expiry timestamp in ISO 8601 format. Standard session duration is 24 hours, but it can be extended up to 30 days. --- .../get-support-session/20get_support_session | 10 +++++++- .../get-support-session/validate-output.json | 6 +++++ docs/core/subscription.md | 23 +++++++++++++++---- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session index b82291bb8..7b1530084 100755 --- a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session +++ b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session @@ -9,6 +9,13 @@ if [[ "$(systemctl is-active support.service)" == "active" ]]; then jactive=true + if [[ "$(systemctl is-active support-expire.timer)" == "active" ]]; then + # 24 hours expiry + expires_at=$(date --utc -d "$(systemctl show --timestamp=utc -P ActiveEnterTimestamp support-expire.timer) + 24 hours" --iso-8601=seconds) + else + # 30 days expiry + expires_at=$(date --utc -d "$(systemctl show --timestamp=utc -P ActiveEnterTimestamp support.service) + 30 days" --iso-8601=seconds) + fi else jactive=false fi @@ -16,4 +23,5 @@ fi jq -c -n \ --argjson active "${jactive}" \ --arg session_id "$(grep VPN_PASSWORD support.env | cut -f 2 -d =)" \ - '{"session_id": $session_id, "active": $active}' + --arg expires_at "${expires_at}" \ + '{"session_id": $session_id, "active": $active, "expires_at": $expires_at}' diff --git a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/validate-output.json b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/validate-output.json index 66025b8d4..e0b46b073 100644 --- a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/validate-output.json +++ b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/validate-output.json @@ -6,10 +6,12 @@ "examples": [ { "session_id": "811630de-67f4-5b6d-8b2f-7cc01f592b1b", + "expires_at": "2026-03-03T15:53:22+00:00", "active": true }, { "session_id": "811630de-67f4-5b6d-8b2f-7cc01f592b1b", + "expires_at": "", "active": false } ], @@ -23,6 +25,10 @@ "type": "string", "description": "Last opened session identifier for the support team" }, + "expires_at": { + "type":"string", + "description": "Scheduled and automatic session stop time, in ISO 8601 format. An empty string is returned if support is not active." + }, "active": { "type": "boolean", "description": "If the opened session is still active or not" diff --git a/docs/core/subscription.md b/docs/core/subscription.md index 482dd5f2c..defcbc020 100644 --- a/docs/core/subscription.md +++ b/docs/core/subscription.md @@ -80,12 +80,11 @@ with this command: systemctl start support systemctl status support -The second command prints a journal excerpt that should contain the session ID. - When a node support session is started -- support SSH key is added to the node /root/.ssh/authorized_keys file. - Only connections from the node itself are allowed with that key +- support SSH key is added to the node /root/.ssh/authorized_keys file + with 24 hours of validity. Only connections from the node itself are + allowed with that key. - support credentials are enabled for cluster-admin access. The user name is defined in Redis. Type `HGET cluster/subscription support_user` to see its value, by default it is `nethsupport`. The password is set to @@ -100,6 +99,22 @@ When the support session is terminated - cluster-admin access is removed - SSH key is removed +The support session is automatically terminated after 24 hours. To avoid +automatic termination and allow it to run up to the maximum allowed +duration of 30 days, execute this command on the relevant node: + + systemctl stop support-expire.timer + +Edit `/root/.ssh/authorized_keys` and increase `expiry-time=` to extend +the SSH key validity. + +Note that after 30 days the session is terminated in any case. + +Inspect the node session expiry with the `get-support-session` action. For +example: + + api-cli run node/7/get-support-session + ## OpenVPN support client The Systemd unit `support.service` controls an OpenVPN client running in a From ce86cff6d0e251f352e774e5c3330f57807ee646 Mon Sep 17 00:00:00 2001 From: Andrea Leardini Date: Tue, 3 Mar 2026 16:55:36 +0100 Subject: [PATCH 4/5] feat(ui): show support session expiration --- core/ui/public/i18n/en/translation.json | 2 +- core/ui/public/i18n/it/translation.json | 2 +- core/ui/src/i18n/index.js | 24 +++++++++++++ .../views/settings/SettingsSubscription.vue | 35 ++++++++++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index d0afa9188..7f6831f1b 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -731,7 +731,7 @@ "start_session_support": "Start session", "session_id": "Session ID", "paste_session_id_to_support_team": "Paste the session ID to the support team", - "remote_support_in_progress": "Remote support session in progress", + "remote_support_in_progress": "The remote support session will end automatically in {time}. You can end it manually at any time.", "stop_session_support": "End session", "remove_cluster_subscription_title": "Remove the cluster subscription", "remove_cluster_subscription_description": "You are going to remove the subscription plan. This will disable the subscription features.", diff --git a/core/ui/public/i18n/it/translation.json b/core/ui/public/i18n/it/translation.json index 9582a52f1..35e817852 100644 --- a/core/ui/public/i18n/it/translation.json +++ b/core/ui/public/i18n/it/translation.json @@ -1615,7 +1615,7 @@ "remove_cluster_subscription_title": "Rimuovi la subscription del cluster", "no_expiration": "Senza scadenza", "paste_session_id_to_support_team": "Incolla l'ID di sessione al team di supporto", - "remote_support_in_progress": "Sessione di supporto remoto in corso", + "remote_support_in_progress": "La sessione di supporto remoto terminerà automaticamente tra {time}. Puoi terminarla manualmente in qualsiasi momento.", "remote_support": "Supporto remoto", "plan_name": "Piano", "request_subscription": "Registra", diff --git a/core/ui/src/i18n/index.js b/core/ui/src/i18n/index.js index e86c7877e..14313a05a 100644 --- a/core/ui/src/i18n/index.js +++ b/core/ui/src/i18n/index.js @@ -16,3 +16,27 @@ export async function loadLanguage(lang) { } } } + +export async function getDateFnsLocale() { + const lang = navigator.language; + try { + const mod = await import( + /* webpackChunkName: "date-fns-locale-[request]" */ + `date-fns/locale/${lang}` + ); + return mod.default || mod; + } catch { + // try base language (e.g. "it" from "it-IT") + const baseLang = lang.split("-")[0]; + try { + const mod = await import( + /* webpackChunkName: "date-fns-locale-[request]" */ + `date-fns/locale/${baseLang}` + ); + return mod.default || mod; + } catch { + const mod = await import("date-fns/locale/en-US"); + return mod.default || mod; + } + } +} \ No newline at end of file diff --git a/core/ui/src/views/settings/SettingsSubscription.vue b/core/ui/src/views/settings/SettingsSubscription.vue index 637c8ec09..b09f5b2ae 100644 --- a/core/ui/src/views/settings/SettingsSubscription.vue +++ b/core/ui/src/views/settings/SettingsSubscription.vue @@ -295,7 +295,9 @@
{{ - $t("settings_subscription.remote_support_in_progress") + $t("settings_subscription.remote_support_in_progress", { + time: formatExpiryDate(sessionExpireDate), + }) }}
{ + this.dateFnsLocale = locale; + }); }, beforeRouteEnter(to, from, next) { next((vm) => { @@ -424,6 +435,27 @@ export default { next(); }, methods: { + formatExpiryDate(dateString) { + const parsedDate = parseISO(dateString); + const duration = intervalToDuration({ + start: new Date(), + end: parsedDate, + }); + + const dateTimeStr = new Intl.DateTimeFormat(navigator.language, { + hour: "2-digit", + minute: "2-digit", + day: "2-digit", + month: "2-digit", + year: "numeric", + }).format(parsedDate); + const durationStr = formatDuration(duration, { + format: ["days", "hours"], + delimiter: ", ", + locale: this.dateFnsLocale, + }); + return `${durationStr} (${dateTimeStr})`; + }, showRemoveSubcriptionModal() { this.isShownRemoveSubcription = true; }, @@ -693,6 +725,7 @@ export default { const output = taskResult.output; this.session_id = output.session_id; this.active = output.active; + this.sessionExpireDate = output.expires_at; }, async startSessionSupport() { From 93a535698e20fa7dda16e95094fdd51df718dc60 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 4 Mar 2026 16:48:14 +0100 Subject: [PATCH 5/5] fix: set session age hard limit at 7 days - Decrease session age hard limit from 30 to 7 days - Extend SSH key expire period to 7 days - Update subscription docs --- .../etc/systemd/system/support.service | 2 +- .../get-support-session/20get_support_session | 4 ++-- .../lib/nethserver/node/bin/support-sshkeyctl | 2 +- docs/core/subscription.md | 17 ++++++++--------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/core/imageroot/etc/systemd/system/support.service b/core/imageroot/etc/systemd/system/support.service index 9f39e587a..d1d496909 100644 --- a/core/imageroot/etc/systemd/system/support.service +++ b/core/imageroot/etc/systemd/system/support.service @@ -37,4 +37,4 @@ ExecStopPost=-/usr/bin/systemctl --no-block stop %N-expire.timer PIDFile=%t/support.pid Type=forking SyslogIdentifier=support -RuntimeMaxSec=30d +RuntimeMaxSec=7d diff --git a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session index 7b1530084..9ab2d920b 100755 --- a/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session +++ b/core/imageroot/var/lib/nethserver/node/actions/get-support-session/20get_support_session @@ -13,8 +13,8 @@ if [[ "$(systemctl is-active support.service)" == "active" ]]; then # 24 hours expiry expires_at=$(date --utc -d "$(systemctl show --timestamp=utc -P ActiveEnterTimestamp support-expire.timer) + 24 hours" --iso-8601=seconds) else - # 30 days expiry - expires_at=$(date --utc -d "$(systemctl show --timestamp=utc -P ActiveEnterTimestamp support.service) + 30 days" --iso-8601=seconds) + # 7 days expiry + expires_at=$(date --utc -d "$(systemctl show --timestamp=utc -P ActiveEnterTimestamp support.service) + 7 days" --iso-8601=seconds) fi else jactive=false diff --git a/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl b/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl index ab594dc98..437e628d8 100755 --- a/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl +++ b/core/imageroot/var/lib/nethserver/node/bin/support-sshkeyctl @@ -16,7 +16,7 @@ pubkey_file=${3:-${AGENT_INSTALL_DIR:?}/etc/support/ssh-rsa.pub} function add_ssh_key { local keyopts - keyopts=$(printf 'expiry-time="%s",from="127.0.0.1"' "$(date --utc -d '24 hours' +'%Y%m%d%H%M%SZ')") + keyopts=$(printf 'expiry-time="%s",from="127.0.0.1"' "$(date --utc -d '7 days' +'%Y%m%d%H%M%SZ')") umask 077 printf "%s %s %s\n" "${keyopts}" "$(cat "${pubkey_file}")" "${key_comment}" >> "${authkeys_file}" } diff --git a/docs/core/subscription.md b/docs/core/subscription.md index defcbc020..769c3acfd 100644 --- a/docs/core/subscription.md +++ b/docs/core/subscription.md @@ -82,13 +82,15 @@ with this command: When a node support session is started -- support SSH key is added to the node /root/.ssh/authorized_keys file - with 24 hours of validity. Only connections from the node itself are - allowed with that key. +- support SSH key is added to the node `/root/.ssh/authorized_keys` file + with an `expiry-time=` attribute. Only connections from the node itself + are allowed with that key. - support credentials are enabled for cluster-admin access. The user name is defined in Redis. Type `HGET cluster/subscription support_user` to see its value, by default it is `nethsupport`. The password is set to - the support session ID value + the support session ID value. `nethsupport` can connect from localhost + and a restricted set of IP addresses, coded in the + `support-clusteradminctl` command. - an OpenVPN tunnel is established with the support server, allowing connections to sshd and cluster-admin. The support server address and port can be overridden in the Redis `cluster/subscription` key @@ -101,14 +103,11 @@ When the support session is terminated The support session is automatically terminated after 24 hours. To avoid automatic termination and allow it to run up to the maximum allowed -duration of 30 days, execute this command on the relevant node: +duration of 7 days, execute this command on the relevant node: systemctl stop support-expire.timer -Edit `/root/.ssh/authorized_keys` and increase `expiry-time=` to extend -the SSH key validity. - -Note that after 30 days the session is terminated in any case. +After 7 days the session is terminated unconditionally. Inspect the node session expiry with the `get-support-session` action. For example: