diff options
author | Christian Poessinger <christian@poessinger.com> | 2022-04-09 13:42:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-09 13:42:17 +0200 |
commit | 139ab816466053d73cb8a68fe4af3de39d98d306 (patch) | |
tree | 3c4d301633c2307e677b4266dfe1ca5094717b76 /src | |
parent | f9ebccd2ef0b144617b5f4acf79d37502114d34a (diff) | |
parent | 1da9cc02d7c83898c267070618e2cc91e16eb1cf (diff) | |
download | vyos-1x-139ab816466053d73cb8a68fe4af3de39d98d306.tar.gz vyos-1x-139ab816466053d73cb8a68fe4af3de39d98d306.zip |
Merge pull request #1242 from goodNETnick/ocserv_local_otp
ocserv: T4231: Added OTP support for Openconnect 2FA
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 80 | ||||
-rwxr-xr-x | src/migration-scripts/openconnect/1-to-2 | 54 |
2 files changed, 126 insertions, 8 deletions
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 51ea1f223..84d31f9a5 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -24,6 +24,7 @@ from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import is_systemd_service_running +from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 @@ -35,6 +36,7 @@ airbag.enable() cfg_dir = '/run/ocserv' ocserv_conf = cfg_dir + '/ocserv.conf' ocserv_passwd = cfg_dir + '/ocpasswd' +ocserv_otp_usr = cfg_dir + '/users.oath' radius_cfg = cfg_dir + '/radiusclient.conf' radius_servers = cfg_dir + '/radius_servers' @@ -54,6 +56,16 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + del ocserv['authentication']['local_users']['username']['otp'] + if not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at least one user') + default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] + for user, params in ocserv['authentication']['local_users']['username'].items(): + # Not every configuration requires OTP settings + if ocserv['authentication']['local_users']['username'][user].get('otp'): + ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) + if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) @@ -63,17 +75,34 @@ def get_config(): def verify(ocserv): if ocserv is None: return None - # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: if "local" in ocserv["authentication"]["mode"]: - if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('openconnect mode local required at leat one user') + if "radius" in ocserv["authentication"]["mode"]: + raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration') + if not ocserv["authentication"]["local_users"]: + raise ConfigError('openconnect mode local required at least one user') + if not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at least one user') else: - for user in ocserv["authentication"]["local_users"]["username"]: - if not "password" in ocserv["authentication"]["local_users"]["username"][user]: - raise ConfigError(f'password required for user {user}') + # For OTP mode: verify that each local user has an OTP key + if "otp" in ocserv["authentication"]["mode"]["local"]: + users_wo_key = [] + for user, user_config in ocserv["authentication"]["local_users"]["username"].items(): + # User has no OTP key defined + if dict_search('otp.key', user_config) == None: + users_wo_key.append(user) + if users_wo_key: + raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}') + # For password (and default) mode: verify that each local user has password + if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]: + users_wo_pswd = [] + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + users_wo_pswd.append(user) + if users_wo_pswd: + raise ConfigError(f'password required for users:\n{users_wo_pswd}') else: raise ConfigError('openconnect authentication mode required') else: @@ -122,7 +151,6 @@ def verify(ocserv): else: raise ConfigError('openconnect network settings required') - def generate(ocserv): if not ocserv: return None @@ -132,6 +160,42 @@ def generate(ocserv): render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"]) # Render radius servers render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"]) + elif "local" in ocserv["authentication"]["mode"]: + # if mode "OTP", generate OTP users file parameters + if "otp" in ocserv["authentication"]["mode"]["local"]: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + # OTP token type from CLI parameters: + otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval")) + token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type") + otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length")) + if token_type == "hotp-time": + otp_type = "HOTP/T" + otp_interval + elif token_type == "hotp-event": + otp_type = "HOTP/E" + else: + otp_type = "HOTP/T" + otp_interval + ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length + # if there is a password, generate hash + if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) + + if "password-otp" in ocserv["authentication"]["mode"]["local"]: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) + # Render local users OTP keys + render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.tmpl', ocserv["authentication"]["local_users"]) + elif "password" in ocserv["authentication"]["mode"]["local"]: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) + elif "otp" in ocserv["authentication"]["mode"]["local"]: + # Render local users OTP keys + render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.tmpl', ocserv["authentication"]["local_users"]) + else: + # Render local users ocpasswd + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) else: if "local_users" in ocserv["authentication"]: for user in ocserv["authentication"]["local_users"]["username"]: @@ -169,7 +233,7 @@ def generate(ocserv): def apply(ocserv): if not ocserv: call('systemctl stop ocserv.service') - for file in [ocserv_conf, ocserv_passwd]: + for file in [ocserv_conf, ocserv_passwd, ocserv_otp_usr]: if os.path.exists(file): os.unlink(file) else: diff --git a/src/migration-scripts/openconnect/1-to-2 b/src/migration-scripts/openconnect/1-to-2 new file mode 100755 index 000000000..7031fb252 --- /dev/null +++ b/src/migration-scripts/openconnect/1-to-2 @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Delete depricated outside-nexthop address + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'openconnect'] + +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + if config.exists(cfg_base + ['authentication', 'mode']): + if config.return_value(cfg_base + ['authentication', 'mode']) == 'radius': + # if "mode value radius", change to "tag node mode + valueless node radius" + config.delete(cfg_base + ['authentication','mode', 'radius']) + config.set(cfg_base + ['authentication', 'mode', 'radius'], value=None, replace=True) + elif not config.exists(cfg_base + ['authentication', 'mode', 'local']): + # if "mode local", change to "tag node mode + node local value password" + config.delete(cfg_base + ['authentication', 'mode', 'local']) + config.set(cfg_base + ['authentication', 'mode', 'local'], value='password', replace=True) + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) |