feat: add a setting for sudo over ssh

This commit is contained in:
Felitendo committed 2026-09-28 14:38:02 +02:00
1 parent 658adb7da7
commit d8bbefed8a
22 files changed
+1367 -1320

No files matched your search

+1
View File
@@ -50,6 +50,7 @@ Settings Settings::load(const QString &path)
s.maxFailures = kv.integer(QStringLiteral("MaxFailures"), s.maxFailures, 1, 20);
s.lockoutMinutes = kv.integer(QStringLiteral("LockoutMinutes"), s.lockoutMinutes, 1, 24 * 60);
s.skipLidClosed = kv.boolean(QStringLiteral("SkipLidClosed"), s.skipLidClosed);
s.sshSessions = kv.boolean(QStringLiteral("SshSessions"), s.sshSessions);
s.adapt = kv.boolean(QStringLiteral("Adapt"), s.adapt);
return s;
}
+4
View File
@@ -44,6 +44,10 @@ struct Settings {
int lockoutMinutes = 15;
// A laptop with the lid shut has its camera looking at the keyboard.
bool skipLidClosed = true;
// sudo and admin prompts in an SSH session. Off by default: whoever sits
// in front of the camera need not be whoever types. On, that person still
// has to be at the machine (see the PAM module).
bool sshSessions = false;
// Take in a little of each confident unlock, so a new haircut or a pair
// of glasses does not need a new setup. Face ID does the same.
bool adapt = true;
+9 -3
View File
@@ -7,9 +7,10 @@
// hello version
// status [user] faces, lockout, camera, settings
// cameras every camera and which one is in use
// verify user purpose [verbose]
// verify user purpose [verbose] [remote]
// scan for that user. Events: started, face,
// hint, frame (verbose only), result
// hint, frame (verbose only), result. remote:
// the PAM module asks for an SSH session
// enroll [name] set up a face for the caller. Asks polkit
// first. Events: authorizing, authorized,
// started, frame, pose, hint, captured, result.
@@ -434,12 +435,17 @@ void Server::handleVerify(Client *client, const QJsonObject &request)
if (!targetUser(client, request, &uid)) {
return;
}
const Settings s = settings();
// Whoever sits in front of the camera need not be whoever types.
if (request.value(u"remote").toBool() && !s.sshSessions) {
fail(client, QStringLiteral("remote"));
return;
}
if (m_job) {
fail(client, QStringLiteral("busy"));
return;
}
const Settings s = settings();
const qint64 now = QDateTime::currentSecsSinceEpoch();
const UserState state = UserState::load(m_options.stateDir, uid);
if (const qint64 left = state.lockoutLeft(now)) {
+2
View File
@@ -309,6 +309,7 @@ FU_SETTINGS=(
"pam|sudo|bool|no|sudo in a terminal"
"pam|polkit-1|bool|no|Admin prompts"
"pam|lockscreens|bool|yes|Lock screens|||lockers"
"sys|SshSessions|bool|no|In SSH sessions"
"group|Recognition"
"sys|Liveness|choice|light|Photo check|off,light,heavy"
"sys|Strictness|choice|normal|How closely the face has to match|relaxed,normal,strict"
@@ -348,6 +349,7 @@ fu_setting_help() {
LockBlur:*) fu_msg "Blurs the picture behind face-unlock's own lock screen." ;;
lockscreens:*) fu_msg "The lock screen takes your face in its password check. Press Enter on the empty field to scan. Found here: %s." "$(fu_pam_lockers_list)" ;;
polkit-1:*) fu_msg "The password windows of your desktop and apps, for example when you install software. No match: you type the password." ;;
SshSessions:*) fu_msg "sudo in an SSH session takes your face too, if you sit at this computer. Off by default: the person at the camera may not be the one typing." ;;
Liveness:heavy) fu_msg "You have to blink or turn your head a little. This also stops printed photos." ;;
Liveness:off) fu_msg "No check at all. Only for trying out a camera." ;;
Liveness:*) fu_msg "Stops photos on a phone, a tablet or glossy paper. You do not have to blink. A matte printed photo can get through." ;;
+1
View File
@@ -28,6 +28,7 @@ declare -A FU_SYS_VALID=(
[ScanSeconds]='^([2-9]|1[0-5])$'
[Adapt]='^(yes|no)$'
[SkipLidClosed]='^(yes|no)$'
[SshSessions]='^(yes|no)$'
[MaxFailures]='^([1-9]|1[0-9]|20)$'
[LockoutMinutes]='^[1-9][0-9]{0,3}$'
)
+10 -12
View File
@@ -10,10 +10,11 @@
// It refuses to be useful in exactly the places where a camera is the wrong
// witness:
// - a remote login (SSH, a remote host in PAM_RHOST). Whoever is in front
// of the camera is not the person typing.
// of the camera need not be the person typing. The daemon refuses it
// unless SshSessions is on in the system settings.
// - a user who is not sitting at the machine right now, with an active
// session on a seat.
// In both cases it returns PAM_IGNORE without touching the camera.
// session on a seat. Then it returns PAM_IGNORE without touching the
// camera.
//
// Options:
// purpose=sudo|polkit|other what the bubble says (default: from the
@@ -291,12 +292,7 @@ __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_authenticate(pam_ha
}
return PAM_IGNORE;
}
if (is_remote(pamh)) {
if (o.debug) {
pam_syslog(pamh, LOG_DEBUG, "remote session, not scanning for %s", user);
}
return PAM_IGNORE;
}
const bool remote = is_remote(pamh);
if (!is_at_seat(user)) {
if (o.debug) {
pam_syslog(pamh, LOG_DEBUG, "%s is not at the machine, not scanning", user);
@@ -310,7 +306,8 @@ __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_authenticate(pam_ha
}
char request[512];
const int len = snprintf(request, sizeof(request), "{\"cmd\":\"verify\",\"user\":\"%s\",\"purpose\":\"%s\"}\n", user, purpose);
const int len = snprintf(request, sizeof(request), "{\"cmd\":\"verify\",\"user\":\"%s\",\"purpose\":\"%s\",\"remote\":%s}\n", user, purpose,
remote ? "true" : "false");
if (len <= 0 || (size_t)len >= sizeof(request) || strpbrk(user, "\"\\") || write(fd, request, (size_t)len) != len) {
close(fd);
return PAM_IGNORE;
@@ -380,8 +377,9 @@ __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_authenticate(pam_ha
} else if (strcmp(value, "no-face") == 0 || strcmp(value, "attention") == 0 || strcmp(value, "quality") == 0) {
rc = PAM_AUTH_ERR;
} else {
// Not set up, no camera, lid shut, busy: face unlock is
// simply not available right now.
// Not set up, no camera, the camera in a video call,
// lid shut, busy, SSH: face unlock is simply not
// available right now.
rc = PAM_AUTHINFO_UNAVAIL;
if (o.debug) {
pam_syslog(pamh, LOG_DEBUG, "face unlock unavailable for %s: %s", user, value);