Allow SSH keys authentication for SFTP(Go)

This commit is contained in:
Miraty 2023-06-15 03:35:42 +02:00
parent 256bd51e0f
commit 067e1ccf42
13 changed files with 255 additions and 57 deletions

View file

@ -24,7 +24,7 @@ I plan to create and maintain a public stable instance of ServNest, but I haven'
### Name server (`ns`)
* Host a zone on the server
* Zone file edition through `<textarea>`
* Plain zone file edition
* Dedicated forms to set/unset `A`, `AAAA`, `NS`, `TXT`, `CAA`, `SRV`, `MX`, `SRV`, `SSHFP`, `TLSA`, `CNAME`, `DNAME` and `LOC` records
* Display records or the full zone file

View file

@ -41,6 +41,14 @@ input[type=password] {
width: 7ch;
}
#public-key {
width: 70ch;
}
#key {
width: 65ch;
}
:disabled {
cursor: not-allowed;
}

View file

@ -36,7 +36,7 @@ h3 {
font-size: 1.1rem;
}
main > *:not(pre, form), form > *:not(textarea), footer {
main > *:not(pre, form), footer {
max-width: 40rem;
margin-left: auto;
margin-right: auto;

View file

@ -0,0 +1,11 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "ssh-keys" (
"key" TEXT NOT NULL,
"username" TEXT NOT NULL,
"directory" TEXT NOT NULL,
UNIQUE("key", "username", "directory"),
FOREIGN KEY("username") REFERENCES "users"("id")
);
COMMIT;

View file

@ -53,4 +53,11 @@ CREATE TABLE IF NOT EXISTS "sites" (
PRIMARY KEY("address", "type"),
FOREIGN KEY("username") REFERENCES "users"("id")
);
CREATE TABLE IF NOT EXISTS "ssh-keys" (
"key" TEXT NOT NULL,
"username" TEXT NOT NULL,
"directory" TEXT NOT NULL,
UNIQUE("key", "username", "directory"),
FOREIGN KEY("username") REFERENCES "users"("id")
);
COMMIT;

View file

@ -1,6 +1,7 @@
<?php
const SUBPATH_REGEX = '^[a-z0-9-]{4,63}$';
const ED25519_PUBKEY_REGEX = '^[a-zA-Z0-9/+]{68}$';
function htSetupUserFs($id) {
// Setup SFTP directory
@ -37,6 +38,8 @@ function formatDomain($domain) {
}
function listFsDirs($username) {
if ($username === '')
return [];
$absoluteDirs = glob(CONF['ht']['ht_path'] . '/fs/' . $username . '/*/', GLOB_ONLYDIR);
$dirs = [];
foreach ($absoluteDirs as $absoluteDir)

View file

@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-04 23:57+0200\n"
"POT-Creation-Date: 2023-06-15 01:33+0200\n"
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -274,18 +274,26 @@ msgstr "Supprimer un accès"
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
msgstr "Retirer un accès HTTP existant d'un sous-dossier de l'espace SFTP"
#: router.php:152 view.php:39
#: pages.php:197
msgid "Manage SSH keys"
msgstr "Gérer les clés SSH"
#: pages.php:198
msgid "Choose what SSH key can edit what directory"
msgstr "Choisir quelle clé SSH peut modifier quel dossier"
#: router.php:68
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
#: router.php:106 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
msgstr "Ce service est en cours de maintenance. Aucune action ne peut être effectuée avant qu'ane administrataire termine de le réparer."
#: router.php:162
#: router.php:115
msgid "You need to be logged in to do this."
msgstr "Vous devez être connecté·e à un compte pour faire cela."
#: router.php:164
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
#: view.php:19
msgid "You are using a testing account. It may be deleted anytime."
msgstr "Vous utilisez un compte de test. Il risque d'être supprimé n'importe quand."
@ -308,11 +316,16 @@ msgstr "Ce formulaire ne sera pas accepté car il faut %sse connecter%s d'abord.
msgid "%sSource code%s available under %s."
msgstr "%sCode source%s disponible sous %s."
#: fn/auth.php:110
#: fn/auth.php:95
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr "Votre compte ne peut pas être supprimé car le service %s est actuellement indisponible."
#: fn/auth.php:143
msgid "Account rate limit reached, try again later."
msgstr "Limite de taux pour ce compte atteinte, réessayez plus tard."
#: fn/auth.php:135
#: fn/auth.php:168
msgid "Global rate limit reached, try again later."
msgstr "Limite de taux globale atteinte, réessayez plus tard."
@ -328,15 +341,15 @@ msgstr "<strong>Erreur de l'utilisataire</strong>&nbsp;: "
msgid "<strong>Server error</strong>: "
msgstr "<strong>Erreur du serveur</strong>&nbsp;: "
#: fn/common.php:129
#: fn/common.php:132
msgid "Wrong proof."
msgstr "Preuve incorrecte."
#: fn/dns.php:63
#: fn/dns.php:64
msgid "IP address malformed."
msgstr "Adresse IP malformée."
#: fn/dns.php:68 fn/ht.php:30
#: fn/dns.php:69 fn/ht.php:31
msgid "Domain malformed."
msgstr "Domaine malformé."
@ -396,11 +409,6 @@ msgid "Account deletion must be confirmed."
msgstr "La suppression du compte doit être confirmée."
#: pg-act/auth/unregister.php:13
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr "Votre compte ne peut pas être supprimé car le service %s est actuellement indisponible."
#: pg-act/auth/unregister.php:42
msgid "Account deleted."
msgstr "Compte supprimé."
@ -453,6 +461,18 @@ msgstr "Ce chemin est déjà pris sur ce service. Utilisez-en un autre."
msgid "Access removed."
msgstr "Accès retiré."
#: pg-act/ht/keys.php:13
msgid "Path is not valid."
msgstr "Le chemin n'est pas valide."
#: pg-act/ht/keys.php:15
msgid "Ed25519 public key seems wrongly formatted."
msgstr "La clé public Ed25519 semble mal formattée."
#: pg-act/ht/keys.php:39
msgid "SSH keys updated."
msgstr "Clés SSH mises à jour."
#: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16
#: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20
#: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25
@ -813,6 +833,30 @@ msgstr "Approuvé"
msgid "Stable Let's Encrypt certificates"
msgstr "Vrai certificat Let's Encrypt"
#: pg-view/ht/keys.php:2
msgid "In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content."
msgstr "En plus de la clé de passe, c'est également possible d'accéder à l'espace SFTP en utilisant des clés SSH Ed25519. Une clé peut être autorisée à modifier dans tout l'espace (<code>/</code>) ou dans un quelconque sous-dossier spécifique. Une clé est toujours autorisée à lister le contenu de n'importe quel dossier."
#: pg-view/ht/keys.php:17
msgid "Add new SSH key access"
msgstr "Ajouter un nouvel accès par clé SSH"
#: pg-view/ht/keys.php:17
msgid "SSH key access"
msgstr "Accès par clé SSH"
#: pg-view/ht/keys.php:19
msgid "Public key"
msgstr "Clé publique"
#: pg-view/ht/keys.php:23
msgid "Allowed directory"
msgstr "Dossier autorisé"
#: pg-view/ht/keys.php:30
msgid "Update"
msgstr "Mettre à jour"
#: pg-view/ns/caa.php:3
msgid "Flag"
msgstr "Flag"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-04 23:57+0200\n"
"POT-Creation-Date: 2023-06-15 01:33+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -286,18 +286,26 @@ msgstr ""
msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
msgstr ""
#: router.php:152 view.php:39
#: pages.php:197
msgid "Manage SSH keys"
msgstr ""
#: pages.php:198
msgid "Choose what SSH key can edit what directory"
msgstr ""
#: router.php:68
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr ""
#: router.php:106 view.php:39
msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
msgstr ""
#: router.php:162
#: router.php:115
msgid "You need to be logged in to do this."
msgstr ""
#: router.php:164
msgid "This account doesn't exist anymore. Log out to end this ghost session."
msgstr ""
#: view.php:19
msgid "You are using a testing account. It may be deleted anytime."
msgstr ""
@ -320,11 +328,16 @@ msgstr ""
msgid "%sSource code%s available under %s."
msgstr ""
#: fn/auth.php:110
#: fn/auth.php:95
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr ""
#: fn/auth.php:143
msgid "Account rate limit reached, try again later."
msgstr ""
#: fn/auth.php:135
#: fn/auth.php:168
msgid "Global rate limit reached, try again later."
msgstr ""
@ -340,15 +353,15 @@ msgstr ""
msgid "<strong>Server error</strong>: "
msgstr ""
#: fn/common.php:129
#: fn/common.php:132
msgid "Wrong proof."
msgstr ""
#: fn/dns.php:63
#: fn/dns.php:64
msgid "IP address malformed."
msgstr ""
#: fn/dns.php:68 fn/ht.php:30
#: fn/dns.php:69 fn/ht.php:31
msgid "Domain malformed."
msgstr ""
@ -408,11 +421,6 @@ msgid "Account deletion must be confirmed."
msgstr ""
#: pg-act/auth/unregister.php:13
#, php-format
msgid "Your account can't be deleted because the %s service is currently unavailable."
msgstr ""
#: pg-act/auth/unregister.php:42
msgid "Account deleted."
msgstr ""
@ -465,6 +473,18 @@ msgstr ""
msgid "Access removed."
msgstr ""
#: pg-act/ht/keys.php:13
msgid "Path is not valid."
msgstr ""
#: pg-act/ht/keys.php:15
msgid "Ed25519 public key seems wrongly formatted."
msgstr ""
#: pg-act/ht/keys.php:39
msgid "SSH keys updated."
msgstr ""
#: pg-act/ns/caa.php:25 pg-act/ns/cname.php:16 pg-act/ns/dname.php:16
#: pg-act/ns/ip.php:16 pg-act/ns/loc.php:72 pg-act/ns/mx.php:20
#: pg-act/ns/ns.php:16 pg-act/ns/srv.php:28 pg-act/ns/sshfp.php:25
@ -825,6 +845,30 @@ msgstr ""
msgid "Stable Let's Encrypt certificates"
msgstr ""
#: pg-view/ht/keys.php:2
msgid "In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content."
msgstr ""
#: pg-view/ht/keys.php:17
msgid "Add new SSH key access"
msgstr ""
#: pg-view/ht/keys.php:17
msgid "SSH key access"
msgstr ""
#: pg-view/ht/keys.php:19
msgid "Public key"
msgstr ""
#: pg-view/ht/keys.php:23
msgid "Allowed directory"
msgstr ""
#: pg-view/ht/keys.php:30
msgid "Update"
msgstr ""
#: pg-view/ns/caa.php:3
msgid "Flag"
msgstr ""

View file

@ -193,5 +193,10 @@ define('PAGES', [
'title' => _('Delete access'),
'description' => _('Delete an existing HTTP access from a subdirectory of the SFTP space'),
],
'keys' => [
'title' => _('Manage SSH keys'),
'description' => _('Choose what SSH key can edit what directory'),
'tokens_account_cost' => 300,
],
],
]);

View file

@ -26,4 +26,3 @@ $_SESSION['type'] = query('select', 'users', ['id' => $id], 'type')[0];
setupDisplayUsername($_POST['username']);
redir();

39
pg-act/ht/keys.php Executable file
View file

@ -0,0 +1,39 @@
<?php
$el_nb = count($_POST['keys']);
if ($el_nb < 1 OR $el_nb > 8)
output(403, 'Wrong elements number.');
foreach ($_POST['keys'] as $i => $key) {
if (($key['public-key'] ?? '') === '') {
unset($_POST['keys'][$i]);
continue;
}
if (preg_match('#^/[/\p{L}\{M}\p{N}\p{P}\p{S}\p{Zs}]{1,254}$#Du', $key['dir'] ?? '') !== 1)
output(403, _('Path is not valid.'));
if (preg_match('#' . ED25519_PUBKEY_REGEX . '#D', $key['public-key']) !== 1)
output(403, _('Ed25519 public key seems wrongly formatted.'));
}
$keys = array_values($_POST['keys']);
rateLimit();
try {
DB->beginTransaction();
query('delete', 'ssh-keys', ['username' => $_SESSION['id']]);
foreach ($keys as $key)
insert('ssh-keys', [
'key' => $key['public-key'],
'username' => $_SESSION['id'],
'directory' => $key['dir'],
]);
DB->commit();
} catch (Exception $e) {
DB->rollback();
output(500, 'Database error.', [$e->getMessage()]);
}
output(200, _('SSH keys updated.'));

31
pg-view/ht/keys.php Executable file
View file

@ -0,0 +1,31 @@
<p>
<?= _('In addition to your password, you can also access your SFTP space using Ed25519 SSH keys. A key can be granted modification rights to the full space (<code>/</code>) or to any arbitrary subdirectory. A key is always allowed to list any directory content.') ?>
</p>
<form method="post">
<datalist id="dirs">
<option value="/"></option>
<?php
foreach (listFsDirs($_SESSION['id'] ?? '') as $dir)
echo ' <option value="/' . $dir . '"></option>' . LF;
?>
</datalist>
<?php
foreach (array_slice(array_merge(query('select', 'ssh-keys', ['username' => $_SESSION['id'] ?? '']), [['key' => '', 'username' => '', 'directory' => '/']]), 0, 8) as $i => $ssh_key) {
?>
<fieldset>
<legend><?= ($ssh_key['key'] === '') ? _('Add new SSH key access') : _('SSH key access') ?></legend>
<div>
<label for="public-key"><?= _('Public key') ?></label><br>
<code>ssh-ed15519 <input pattern="<?= ED25519_PUBKEY_REGEX ?>" placeholder="AAAAC3NzaC1lZDI1NTE5AAAAI<?= substr(base64_encode(random_bytes(32)), 0, 43) ?>" id="public-key" name="keys[<?= $i ?>][public-key]" value="<?= $ssh_key['key'] ?>" type="text"></code>
</div>
<div>
<label for="dir"><?= _('Allowed directory') ?></label><br>
<input list="dirs" placeholder="/" value="<?= htmlspecialchars($ssh_key['directory']) ?>" id="dir" name="keys[<?= $i ?>][dir]" type="text">
</div>
</fieldset>
<?php
}
?>
<input type="submit" value="<?= _('Update') ?>">
</form>

View file

@ -1,4 +1,4 @@
<?php
<?php // ServNest authenticator for SFTPGo https://github.com/drakkan/sftpgo/blob/main/docs/external-auth.md
const DEBUG = false;
!DEBUG or ob_start();
@ -21,27 +21,34 @@ $username = hashUsername($auth_data['username']);
if (usernameExists($username) !== true)
deny('This username doesn\'t exist.');
if (!in_array('ht', explode(',', query('select', 'users', ['username' => $username], 'services')[0]), true))
$account = query('select', 'users', ['username' => $username])[0];
if (!in_array('ht', explode(',', $account['services']), true))
deny('Service not enabled for this user.');
$id = query('select', 'users', ['username' => $username], 'id')[0];
const SFTPGO_DENY_PERMS = ['/' => ['list']];
const SFTPGO_ALLOW_PERMS = ['list', 'download', 'upload', 'overwrite', 'delete_files', 'delete_dirs', 'rename_files', 'rename_dirs', 'create_dirs', 'chtimes'];
if ($auth_data['password'] !== '') {
if (checkPassword($account['id'], $auth_data['password']) !== true)
deny('Wrong password.');
$permissions['/'] = SFTPGO_ALLOW_PERMS;
} else if ($auth_data['public_key'] !== '') {
$permissions = SFTPGO_DENY_PERMS;
foreach (query('select', 'ssh-keys', ['username' => $account['id']]) as $key)
if (hash_equals('ssh-ed25519 ' . $key['key'] . LF, $auth_data['public_key']))
$permissions[$key['directory']] = SFTPGO_ALLOW_PERMS;
if ($permissions === SFTPGO_DENY_PERMS)
deny('No matching SSH key allowed.');
} else
deny('Unknown authentication method.');
if (checkPassword($id, $auth_data['password']) !== true)
deny('Wrong password.');
echo json_encode([
'status' => 1,
'username' => $auth_data['username'],
'home_dir' => CONF['ht']['ht_path'] . '/fs/' . $account['id'],
'quota_size' => ($account['type'] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing'],
'permissions' => $permissions,
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
echo '
{
"status": 1,
"username": ' . json_encode($auth_data['username']) . ',
"home_dir": "' . CONF['ht']['ht_path'] . '/fs/' . $id . '",
"quota_size": ' . ((query('select', 'users', ['id' => $id], 'type')[0] === 'approved') ? CONF['ht']['user_quota_approved'] : CONF['ht']['user_quota_testing']) . ',
"permissions": {
"/": [
"*"
]
}
}
';
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents());
!DEBUG or file_put_contents(ROOT_PATH . '/db/debug.txt', ob_get_contents() . 'accepted');
http_response_code(200);