From 11c488215620d12c1f79fc9b05deb9928d2cab39 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 16 Nov 2020 14:03:21 +0100 Subject: [sysconfig] SSH: Split pubkey and rest of config, add more options Now we can have exactly one SSH-Config per sysconfig, which avoids confusion due to config mismatch regarding "allow pw" and "port". The install include takes care of splitting the key into a new module for existing modules, but doesn't remove duplicate SshConfig modules from sysconfigs, as this might lead to additional confusion. Next time the user edits a sysconfig, they are forced to pick exactly one SshConfig module. The "allow password login" option was extended to allow password login for non-root users only in addition to simply being "yes" or "no". There's an additional option that can entirely limit the group of users allowed to log in via SSH. --- .../sysconfig/addmodule_sshconfig.inc.php | 16 ++--- .../sysconfig/addmodule_sshkey.inc.php | 72 ++++++++++++++++++++++ .../sysconfig/inc/configmodule.inc.php | 10 +-- .../sysconfig/inc/configmodule/sshconfig.inc.php | 32 +++++++--- .../sysconfig/inc/configmodule/sshkey.inc.php | 55 +++++++++++++++++ modules-available/sysconfig/inc/configtgz.inc.php | 17 +++-- modules-available/sysconfig/install.inc.php | 44 ++++++++++++- .../sysconfig/lang/de/config-module.json | 9 ++- .../sysconfig/lang/de/template-tags.json | 39 +++++++----- .../sysconfig/lang/en/config-module.json | 9 ++- .../sysconfig/lang/en/template-tags.json | 32 ++++++---- .../sysconfig/templates/sshconfig-start.html | 33 +++++----- .../sysconfig/templates/sshkey-start.html | 18 ++++++ 13 files changed, 307 insertions(+), 79 deletions(-) create mode 100644 modules-available/sysconfig/addmodule_sshkey.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/sshkey.inc.php create mode 100644 modules-available/sysconfig/templates/sshkey-start.html diff --git a/modules-available/sysconfig/addmodule_sshconfig.inc.php b/modules-available/sysconfig/addmodule_sshconfig.inc.php index 495ba2a9..4a75d77e 100644 --- a/modules-available/sysconfig/addmodule_sshconfig.inc.php +++ b/modules-available/sysconfig/addmodule_sshconfig.inc.php @@ -13,10 +13,14 @@ class SshConfig_Start extends AddModule_Base $data = $this->edit->getData(false) + array( 'title' => $this->edit->title(), 'edit' => $this->edit->id(), - 'apl' => $this->edit->getData('allowPasswordLogin') === 'yes' + 'PWD_' . strtoupper($this->edit->getData('allowPasswordLogin')) . '_selected' => 'selected', + 'USR_' . strtoupper($this->edit->getData('allowedUsersLogin')) . '_selected' => 'selected', ); } else { - $data = array(); + $data = array( + 'PWD_NO_selected' => 'selected', + 'USR_ROOT_ONLY_selected' => 'selected', + ); } Render::addDialog(Dictionary::translateFile('config-module', 'sshconfig_title'), false, 'sshconfig-start', $data + array( 'step' => 'SshConfig_Finish', @@ -44,7 +48,8 @@ class SshConfig_Finish extends AddModule_Base Message::addError('main.error-read', 'sshconfig.inc.php'); Util::redirect('?do=SysConfig&action=addmodule&step=SshConfig_Start'); } - $module->setData('allowPasswordLogin', Request::post('allowPasswordLogin') === 'yes'); + $module->setData('allowPasswordLogin', Request::post('allowPasswordLogin')); + $module->setData('allowedUsersLogin', Request::post('allowedUsersLogin')); $port = Request::post('listenPort', ''); if ($port === '') { $port = 22; @@ -53,10 +58,7 @@ class SshConfig_Finish extends AddModule_Base Message::addError('main.value-invalid', 'port', Request::post('listenPort')); Util::redirect('?do=SysConfig&action=addmodule&step=SshConfig_Start'); } - if (!$module->setData('publicKey', Request::post('publicKey'))) { - Message::addError('main.value-invalid', 'pubkey', Request::post('publicKey')); - Util::redirect('?do=SysConfig&action=addmodule&step=SshConfig_Start'); - } + $module->setData('publicKey', false); if ($this->edit !== false) $ret = $module->update($title); else diff --git a/modules-available/sysconfig/addmodule_sshkey.inc.php b/modules-available/sysconfig/addmodule_sshkey.inc.php new file mode 100644 index 00000000..b5ab4ad6 --- /dev/null +++ b/modules-available/sysconfig/addmodule_sshkey.inc.php @@ -0,0 +1,72 @@ +edit !== false) { + $data = $this->edit->getData(false) + array( + 'title' => $this->edit->title(), + 'edit' => $this->edit->id(), + ); + } else { + $data = array(); + } + Render::addDialog(Dictionary::translateFile('config-module', 'sshkey_title'), false, 'sshkey-start', $data + array( + 'step' => 'SshKey_Finish', + )); + } + +} + +class SshKey_Finish extends AddModule_Base +{ + + protected function preprocessInternal() + { + $title = Request::post('title'); + if (empty($title)) { + Message::addError('missing-title'); + return; + } + // Seems ok, create entry + if ($this->edit === false) { + $module = ConfigModule::getInstance('SshKey'); + } else { + $module = $this->edit; + } + if ($module === false) { + Message::addError('main.error-read', 'sshkey.inc.php'); + Util::redirect('?do=SysConfig&action=addmodule&step=SshKey_Start'); + } + if (!$module->setData('publicKey', Request::post('publicKey'))) { + Message::addError('main.value-invalid', 'pubkey', Request::post('publicKey')); + Util::redirect('?do=SysConfig&action=addmodule&step=SshKey_Start'); + } + if ($this->edit !== false) { + $ret = $module->update($title); + } else { + $ret = $module->insert($title); + } + if (!$ret) { + Util::redirect('?do=SysConfig&action=addmodule&step=SshKey_Start'); + } elseif (!$module->generate($this->edit === false, NULL, 200)) { + Util::redirect('?do=SysConfig&action=addmodule&step=SshKey_Start'); + } + // Yay + if ($this->edit !== false) { + Message::addSuccess('module-edited'); + } else { + Message::addSuccess('module-added'); + AddModule_Base::setStep('AddModule_Assign', $module->id()); + return; + } + Util::redirect('?do=SysConfig'); + } + +} diff --git a/modules-available/sysconfig/inc/configmodule.inc.php b/modules-available/sysconfig/inc/configmodule.inc.php index a9035d78..f3906378 100644 --- a/modules-available/sysconfig/inc/configmodule.inc.php +++ b/modules-available/sysconfig/inc/configmodule.inc.php @@ -321,23 +321,25 @@ abstract class ConfigModule * * @return boolean true on success, false otherwise */ - public final function update($title) + public final function update($title = '') { if ($this->moduleId === 0) Util::traceError('ConfigModule::update called when moduleId == 0'); - if (empty($title)) - $title = $this->moduleTitle; + if (!empty($title)) { + $this->moduleTitle = $title; + } if (!$this->validateConfig()) return false; // Update Database::exec("UPDATE configtgz_module SET title = :title, contents = :contents, status = :status, dateline = :now " . " WHERE moduleid = :moduleid LIMIT 1", array( 'moduleid' => $this->moduleId, - 'title' => $title, + 'title' => $this->moduleTitle, 'contents' => json_encode($this->moduleData), 'status' => 'OUTDATED', 'now' => time(), )); + $this->moduleStatus = 'OUTDATED'; return true; } diff --git a/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php b/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php index 9975f789..b5ab20e4 100644 --- a/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php +++ b/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php @@ -5,7 +5,7 @@ ConfigModule::registerModule( Dictionary::translateFileModule('sysconfig', 'config-module', 'sshconfig_title'), // Title Dictionary::translateFileModule('sysconfig', 'config-module', 'sshconfig_description'), // Description Dictionary::translateFileModule('sysconfig', 'config-module', 'group_sshconfig'), // Group - false, // Only one per config? + true, // Only one per config? 500 ); @@ -23,7 +23,6 @@ class ConfigModule_SshConfig extends ConfigModule 'failOnParentFail' => false, 'parent' => $parent ); - // Create config module, which will also check if the pubkey is valid return Taskmanager::submit('SshdConfigGenerator', $config); } @@ -34,25 +33,40 @@ class ConfigModule_SshConfig extends ConfigModule protected function validateConfig() { - return isset($this->moduleData['publicKey']) && isset($this->moduleData['allowPasswordLogin']) && isset($this->moduleData['listenPort']); + // UPGRADE + if (isset($this->moduleData['allowPasswordLogin']) && !isset($this->moduleData['allowedUsersLogin'])) { + $this->moduleData['allowPasswordLogin'] = strtoupper($this->moduleData['allowPasswordLogin']); + if (!in_array($this->moduleData['allowPasswordLogin'], ['NO', 'USER_ONLY', 'YES'])) { + $this->moduleData['allowPasswordLogin'] = 'NO'; + } + $this->moduleData['allowedUsersLogin'] = 'ALL'; + } + return isset($this->moduleData['allowPasswordLogin']) && isset($this->moduleData['allowedUsersLogin']) + && isset($this->moduleData['listenPort']); } public function setData($key, $value) { switch ($key) { case 'publicKey': - break; + if ($value === false) { + error_log('Unsetting publicKey'); + unset($this->moduleData[$key]); + return true; + } + return false; case 'allowPasswordLogin': - if ($value === true || $value === 'yes') - $value = 'yes'; - elseif ($value === false || $value === 'no') - $value = 'no'; - else + if (!in_array($value, ['NO', 'USER_ONLY', 'YES'])) + return false; + break; + case 'allowedUsersLogin'; + if (!in_array($value, ['ROOT_ONLY', 'USER_ONLY', 'ALL'])) return false; break; case 'listenPort': if (!is_numeric($value) || $value < 1 || $value > 65535) return false; + $value = (int)$value; break; default: return false; diff --git a/modules-available/sysconfig/inc/configmodule/sshkey.inc.php b/modules-available/sysconfig/inc/configmodule/sshkey.inc.php new file mode 100644 index 00000000..2d212d25 --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/sshkey.inc.php @@ -0,0 +1,55 @@ +validateConfig()) + return false; + $config = array( + 'files' => [ + '/root/.ssh/authorized_keys.d/sshkey_' . $this->id() . '_' . Util::sanitizeFilename($this->title()) . '.pub' + => $this->moduleData['publicKey']], + 'destination' => $tgz, + 'failOnParentFail' => false, + 'parent' => $parent + ); + // Create config module, which will also check if the pubkey is valid + return Taskmanager::submit('MakeTarball', $config); + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + return isset($this->moduleData['publicKey']); + } + + public function setData($key, $value) + { + switch ($key) { + case 'publicKey': + break; + default: + return false; + } + $this->moduleData[$key] = $value; + return true; + } + +} diff --git a/modules-available/sysconfig/inc/configtgz.inc.php b/modules-available/sysconfig/inc/configtgz.inc.php index ff9e306d..98f29753 100644 --- a/modules-available/sysconfig/inc/configtgz.inc.php +++ b/modules-available/sysconfig/inc/configtgz.inc.php @@ -56,7 +56,9 @@ class ConfigTgz { if (!is_array($moduleIds)) return false; - $this->configTitle = $title; + if (!empty($title)) { + $this->configTitle = $title; + } $this->modules = array(); // Get all modules to put in config $idstr = '0'; // Passed directly in query. Make sure no SQL injection is possible @@ -77,7 +79,7 @@ class ConfigTgz // Update name Database::exec("UPDATE configtgz SET title = :title, status = :status, dateline = :now WHERE configid = :configid LIMIT 1", array( 'configid' => $this->configId, - 'title' => $title, + 'title' => $this->configTitle, 'status' => 'OUTDATED', 'now' => time(), )); @@ -88,9 +90,10 @@ class ConfigTgz * * @param bool $deleteOnError * @param int $timeoutMs + * @param string|null $parentTask parent task to order this rebuild after * @return string|bool true=success, false=error, string=taskid, still running */ - public function generate($deleteOnError = false, $timeoutMs = 0) + public function generate($deleteOnError = false, $timeoutMs = 0, $parentTask = null) { if (!($this->configId > 0) || !is_array($this->modules) || $this->file === false) Util::traceError ('configId <= 0 or modules not array in ConfigTgz::rebuild()'); @@ -103,7 +106,7 @@ class ConfigTgz } } - $task = self::recompress($files, $this->file); + $task = self::recompress($files, $this->file, $parentTask); // Wait for completion if ($timeoutMs > 0 && !Taskmanager::isFailed($task) && !Taskmanager::isFinished($task)) { @@ -215,7 +218,7 @@ class ConfigTgz * @param string $destFile where to store final result * @return false|array taskmanager task */ - private static function recompress($files, $destFile) + private static function recompress($files, $destFile, $parentTask = null) { // Get stuff other modules want to inject $handler = function($hook) { @@ -232,7 +235,9 @@ class ConfigTgz // Hand over to tm return Taskmanager::submit('RecompressArchive', array( 'inputFiles' => $files, - 'outputFile' =>$destFile + 'outputFile' =>$destFile, + 'parentTask' => $parentTask, + 'failOnParentFail' => false, )); } diff --git a/modules-available/sysconfig/install.inc.php b/modules-available/sysconfig/install.inc.php index ace5361b..fe6a8c09 100644 --- a/modules-available/sysconfig/install.inc.php +++ b/modules-available/sysconfig/install.inc.php @@ -120,17 +120,55 @@ if (!tableHasColumn('configtgz', 'warnings')) { } // ----- rebuild configs ------ -// TEMPORARY HACK; Rebuild configs.. move somewhere else? +// PERMANENT HACK; Rebuild configs.. move somewhere else? Module::isAvailable('sysconfig'); $list = ConfigModule::getAll(); +$parentTask = null; +$configList = []; if ($list === false) { - EventLog::warning('Could not regenerate AD/LDAP configs - please do so manually'); + EventLog::warning('Could not regenerate configs - please do so manually'); } else { foreach ($list as $ad) { + if ($ad->moduleType() === 'SshConfig') { + // 2020-11-12: Split SshConfig into SshConfig and SshKey + $pubkey = $ad->getData('publicKey'); + if ($pubkey !== false && !empty($pubkey)) { + error_log('Legacy module with pubkey ' . $ad->id()); + $key = ConfigModule::getInstance('SshKey'); + if ($key !== false) { + $key->setData('publicKey', $pubkey); + if ($key->insert($ad->title())) { + // Insert worked, remove key from old module, add this module to the same configs + $task = $key->generate(false, $parentTask); + if ($task !== false) { + $parentTask = $task; + } + error_log('Inserted new module with id ' . $key->id()); + $ad->setData('publicKey', false); + $ad->update(); + $configs = ConfigTgz::getAllForModule($ad->id()); + foreach ($configs as $config) { + // Add newly created key-only module to all configs + $new = array_merge($config->getModuleIds(), [$key->id()]); + error_log(implode(',', $config->getModuleIds()) . ' -> ' . implode(',', $new)); + $config->update('', $new); + $configList[] = $config; + } + } + } + } + } if ($ad->needRebuild()) { - $ad->generate(false); + $update[] = UPDATE_DONE; + $task = $ad->generate(false, $parentTask); + if ($task !== false) { + $parentTask = $task; + } } } + foreach ($configList as $config) { + $config->generate(false, 0, $parentTask); + } } // Create response for browser diff --git a/modules-available/sysconfig/lang/de/config-module.json b/modules-available/sysconfig/lang/de/config-module.json index f2ed9a90..33c743a5 100644 --- a/modules-available/sysconfig/lang/de/config-module.json +++ b/modules-available/sysconfig/lang/de/config-module.json @@ -9,11 +9,14 @@ "group_branding": "Einrichtungsspezifisches Logo", "group_generic": "Generisch", "group_screensaver": "Bildschirmschoner Styling", - "group_sshconfig": "SSH", + "group_sshconfig": "SSH-Dämon", + "group_sshkey": "SSH-Key", "ldapAuth_description": "Mit diesem Modul l\u00e4sst sich eine generische LDAP-Authentifizierung einrichten.", "ldapAuth_title": "LDAP Authentifizierung", "screensaver_title": "Bildschirmschoner Anpassungen", "screensaver_description": "Mit diesem Modul können sie den Style (QSS) und die Texte des Bildschirmschoners anpassen.", "sshconfig_description": "Mit diesem Modul l\u00e4sst sich steuern, ob und wie der sshd auf den gebooteten Clients startet, und welche Funktionen er zur Verf\u00fcgung stellt. Wenn Sie keinen sshd auf den Clients nutzen wollen, brauchen Sie kein solches Modul zu erstellen.", - "sshconfig_title": "SSH-D\u00e4mon" -} \ No newline at end of file + "sshconfig_title": "SSH-D\u00e4mon", + "sshkey_title": "SSH-Key", + "sshkey_description": "Einen öffentlichen SSH-Schlüssel zu den authorized_keys des root-Benutzers hinzufügen. Mit dem zugehörigen privaten Schlüssel kann dann via SSH auf die gebooteten Clients zugegriffen werden, sofern root-Login im zugehörigen SSH-Dämon-Modul aktiviert wurde." +} diff --git a/modules-available/sysconfig/lang/de/template-tags.json b/modules-available/sysconfig/lang/de/template-tags.json index 0acdb8a7..9637314e 100644 --- a/modules-available/sysconfig/lang/de/template-tags.json +++ b/modules-available/sysconfig/lang/de/template-tags.json @@ -8,8 +8,6 @@ "lang_adText3": "Normalerweise k\u00f6nnen Sie als Bind DN die Kurzform im Format dom\u00e4ne\\benutzer angeben. Wenn dies nicht funktioniert, m\u00fcssen Sie den DN des Benutzers ermitteln. Z.B. unter Eingabe des folgenden Befehls auf einem DC:", "lang_adText4": "Nach Eingabe aller ben\u00f6tigten Daten wird im n\u00e4chsten Schritt \u00fcberpr\u00fcft, ob die Kommunikation mit dem AD m\u00f6glich ist.", "lang_add": "Hinzuf\u00fcgen", - "lang_allowPass": "Login mit Passwort zulassen", - "lang_allowPassInfo": "Wenn aktiviert, l\u00e4sst der sshd Logins mit Benutzername\/Passwort-Kombination zu. Ansonsten werden nur Logins nach dem pubkey-Verfahren zugelassen.", "lang_asteriskMandatory": "Mit (*) gekennzeichnete Felder sind Pflichtfelder", "lang_availableModules": "Verf\u00fcgbare Konfigurationsmodule", "lang_availableSystem": "Verf\u00fcgbare Systemkonfigurationen", @@ -74,10 +72,10 @@ "lang_mapModeNativeFallback": "Nativ in der VM einbinden; Fallback auf VMware Shared Folders", "lang_mapModeNone": "Verzeichnisse nicht durchreichen", "lang_mapModeVmware": "VMware Shared Folders [VMwareTools]", + "lang_modStillUsedBy": "Modul noch in Verwendung durch:", "lang_mode": "Modus", - "lang_modeEasy": "Vereinfachter Modus", "lang_modeAdvanced": "Fortgeschrittener Modus", - "lang_modStillUsedBy": "Modul noch in Verwendung durch:", + "lang_modeEasy": "Vereinfachter Modus", "lang_moduleChoose": "Bitte w\u00e4hlen Sie aus, welche Art Konfigurationsmodul Sie erstellen m\u00f6chten.", "lang_moduleConfiguration": "Konfigurationsmodule", "lang_moduleName": "Modulname", @@ -95,6 +93,7 @@ "lang_noValidCert": "Der Server besitzt kein oder ein nicht valides Zertifikat.", "lang_onProblemSearchBase": "Werden keine Benutzer gefunden, dann \u00fcberpr\u00fcfen Sie bitte die Suchbasis", "lang_or": "oder", + "lang_pwlogin_user_only": "Alle au\u00dfer root", "lang_rebuild": "Neu generieren", "lang_rebuildLong": "Modul oder Konfiguration neu generieren. Das entsprechende Modul bzw. Konfiguration ist aktuell und sollte nicht neu generiert werden m\u00fcssen.", "lang_rebuildOutdatedLong": "Modul oder Konfiguration neu generieren. Das entsprechende Modul bzw. Konfiguration ist veraltet oder nicht vorhanden.", @@ -102,34 +101,34 @@ "lang_replaces": "Ersetzt Modul: ", "lang_restartWizard": "Wizard neu starten", "lang_rootKey": "root pubkey (\u00f6ffentlicher Schl\u00fcssel)", - "lang_rootKeyInfo": "Tragen Sie hier den \u00f6ffentlichen Schl\u00fcssel eines Schl\u00fcsselpaars ein, mit dem Sie sich als root-Benutzer an den Clients anmelden wollen. Lassen Sie das Feld leer, um diese Funktion nicht zu verwenden.", + "lang_rootKeyInfo": "Tragen Sie hier den \u00f6ffentlichen Schl\u00fcssel eines Schl\u00fcsselpaars ein, mit dem Sie sich als root-Benutzer an den Clients anmelden wollen.", "lang_screenBackground": "Hintergrund", "lang_screenBackgroundDescription": " - Ein Hintergrund, bestehend aus einem zweifarbigem Gradienten.", - "lang_screenColor": "Farbe", "lang_screenClock": "Uhr", + "lang_screenColor": "Farbe", "lang_screenDescriptionIdleKill": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der Nutzer ausgeloggt wird.", "lang_screenDescriptionNoTimeout": "Ein Bildschirmschoner ohne Timeout.", "lang_screenDescriptionShutdown": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der PC heruntergefahren oder neugestartet wird.", "lang_screenHeader": "Header", "lang_screenLabel": "Label", "lang_screenLocked": "Sperrbildschirm", - "lang_screenMessageDefaultIdleKill": "Diese Sitzung wird bei Inaktivität in %1 beendet.", + "lang_screenMessageDefaultIdleKill": "Diese Sitzung wird bei Inaktivit\u00e4t in %1 beendet.", "lang_screenMessageDefaultIdleKillLocked": "Diese Sitzung wird in %1 beendet, wenn sie nicht entsperrt wird.", "lang_screenMessageDefaultNoTimeout": "Dieser Bildschirm wird gerade geschont.", "lang_screenMessageDefaultNoTimeoutLocked": "Dieser Rechner ist gesperrt.", "lang_screenMessageDefaultShutdown": "Achtung: Rechner wird in %1 heruntergefahren!", "lang_screenMessageDefaultShutdownLocked": "Achtung: Rechner wird in %1 heruntergefahren!", "lang_screenQss": "QSS", - "lang_screenQssDefault": "#Saver {\n background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #443, stop:1 #000)\n}\n\nQLabel {\n color: #f64;\n}\n\n#lblClock {\n color: #999;\n font-size: 20pt;\n}\n\n#lblHeader {\n font-size: 20pt;\n}\n", - "lang_screenSize": "Gr\u00f6ße", + "lang_screenQssDefault": "#Saver {\r\n background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #443, stop:1 #000)\r\n}\r\n\r\nQLabel {\r\n color: #f64;\r\n}\r\n\r\n#lblClock {\r\n color: #999;\r\n font-size: 20pt;\r\n}\r\n\r\n#lblHeader {\r\n font-size: 20pt;\r\n}\r\n", + "lang_screenSize": "Gr\u00f6\u00dfe", + "lang_screenText": "Inhaltstext Bearbeiten", + "lang_screenTextDefaultIdleKill": "
Keine Nutzeraktivit\u00e4t festgestellt.- - {{lang_sshMultipleHeadsup}} - -