diff options
-rw-r--r-- | data/templates/ocserv/ocserv_config.tmpl | 11 | ||||
-rw-r--r-- | data/templates/ocserv/ocserv_otp_usr.tmpl | 8 | ||||
-rw-r--r-- | interface-definitions/include/auth-local-users.xml.i | 69 | ||||
-rw-r--r-- | interface-definitions/vpn_openconnect.xml.in | 50 | ||||
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 68 | ||||
-rwxr-xr-x | src/migration-scripts/openconnect/1-to-2 | 54 |
6 files changed, 236 insertions, 24 deletions
diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl index 0be805235..19045c4b4 100644 --- a/data/templates/ocserv/ocserv_config.tmpl +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -8,6 +8,14 @@ run-as-group = daemon {% if "radius" in authentication.mode %} auth = "radius [config=/run/ocserv/radiusclient.conf]" +{% elif "local" in authentication.mode %} +{% if authentication.mode.local == "password-otp" %} +auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]" +{% elif authentication.mode.local == "otp" %} +auth = "plain[otp=/run/ocserv/users.oath]" +{% else %} +auth = "plain[/run/ocserv/ocpasswd]" +{% endif %} {% else %} auth = "plain[/run/ocserv/ocpasswd]" {% endif %} @@ -42,7 +50,8 @@ rekey-method = ssl try-mtu-discovery = true cisco-client-compat = true dtls-legacy = true - +max-ban-score = 80 +ban-reset-time = 300 # The name to use for the tun device device = sslvpn diff --git a/data/templates/ocserv/ocserv_otp_usr.tmpl b/data/templates/ocserv/ocserv_otp_usr.tmpl new file mode 100644 index 000000000..db8893ae8 --- /dev/null +++ b/data/templates/ocserv/ocserv_otp_usr.tmpl @@ -0,0 +1,8 @@ +#<token_type> <username> <pin> <secret_hex_key> <counter> <lastpass> <time> +{% if username is defined %} +{% for user, user_config in username.itmes() %} +{% if user_config.disable is not defined and user_config.otp is defined and user_config.otp is not none %} +{{ user_config.otp.token_tmpl }} {{ user }} {{ user_config.otp.pin | default("-", true) }} {{ user_config.otp.key }} +{% endif %} +{% endfor %} +{% endif %} diff --git a/interface-definitions/include/auth-local-users.xml.i b/interface-definitions/include/auth-local-users.xml.i index 8ef09554e..add2fc8e1 100644 --- a/interface-definitions/include/auth-local-users.xml.i +++ b/interface-definitions/include/auth-local-users.xml.i @@ -7,6 +7,10 @@ <tagNode name="username"> <properties> <help>Username used for authentication</help> + <valueHelp> + <format>txt</format> + <description>Username used for authentication</description> + </valueHelp> </properties> <children> #include <include/generic-disable-node.xml.i> @@ -15,6 +19,71 @@ <help>Password used for authentication</help> </properties> </leafNode> + <node name="otp"> + <properties> + <help>2FA OTP authentication parameters</help> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>Token Key Secret key for the token algorithm (see RFC 4226)</help> + <valueHelp> + <format>txt</format> + <description>OTP key in hex-encoded format</description> + </valueHelp> + <constraint> + <regex>[a-fA-F0-9]{20,10000}</regex> + </constraint> + <constraintErrorMessage>Key name must in hex be alphanumerical only (min. 20 hex characters)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="otp-length"> + <properties> + <help>Optional. Number of digits in OTP code (default: 6)</help> + <valueHelp> + <format>u32:6-8</format> + <description>Number of digits in OTP code (default: 6)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 6-8"/> + </constraint> + <constraintErrorMessage>Number of digits in OTP code must be between 6 and 8</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Optional. Time tokens interval in seconds (for time tokens) (default: 30)</help> + <valueHelp> + <format>u32:5-86400</format> + <description>Time tokens interval in seconds (for time tokens). (default: 30)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-86400"/> + </constraint> + <constraintErrorMessage>Time token interval must be between 5 and 86400 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="token-type"> + <properties> + <help>Optional. Token type (default: hotp-time)</help> + <valueHelp> + <format>hotp-time</format> + <description>time-based OTP algorithm</description> + </valueHelp> + <valueHelp> + <format>hotp-event</format> + <description>event-based OTP algorithm</description> + </valueHelp> + <constraint> + <regex>(hotp-time|hotp-event)</regex> + </constraint> + <completionHelp> + <list>hotp-time hotp-event</list> + </completionHelp> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> </children> diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in index 0db5e79d0..a3862647c 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn_openconnect.xml.in @@ -13,25 +13,43 @@ <help>Authentication for remote access SSL VPN Server</help> </properties> <children> - <leafNode name="mode"> + <node name="mode"> <properties> <help>Authentication mode used by this server</help> - <valueHelp> - <format>local</format> - <description>Use local username/password configuration</description> - </valueHelp> - <valueHelp> - <format>radius</format> - <description>Use RADIUS server for user autentication</description> - </valueHelp> - <constraint> - <regex>^(local|radius)$</regex> - </constraint> - <completionHelp> - <list>local radius</list> - </completionHelp> </properties> - </leafNode> + <children> + <leafNode name="local"> + <properties> + <help>Use local username/password configuration (OTP supported)</help> + <valueHelp> + <format>password</format> + <description>Password-only local authentication (default)</description> + </valueHelp> + <valueHelp> + <format>otp</format> + <description>OTP-only local authentication</description> + </valueHelp> + <valueHelp> + <format>password-otp</format> + <description>Password (first) + OTP local authentication</description> + </valueHelp> + <constraint> + <regex>^(password|otp|password-otp)$</regex> + </constraint> + <constraintErrorMessage>Invalid authentication mode</constraintErrorMessage> + <completionHelp> + <list>otp password password-otp</list> + </completionHelp> + </properties> + </leafNode> + <leafNode name="radius"> + <properties> + <help>Use RADIUS server for user autentication</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> #include <include/auth-local-users.xml.i> #include <include/radius-server-ipv4.xml.i> <node name="radius"> diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 51ea1f223..8e100d9f9 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -35,6 +35,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' @@ -63,17 +64,35 @@ 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"]: + if "radius" in ocserv["authentication"]["mode"]: + raise ConfigError('openconnect supports only one authentication mode. Currently configured \'local\' and \'radius\' modes') + if not ocserv["authentication"]["local_users"]: + raise ConfigError('openconnect mode local required at leat one user') + if not ocserv["authentication"]["local_users"]["username"]: raise ConfigError('openconnect mode local required at leat 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 in ocserv["authentication"]["local_users"]["username"]: + if not ocserv["authentication"]["local_users"]["username"][user].get("otp"): + users_wo_key.append(user) + elif not ocserv["authentication"]["local_users"]["username"][user]["otp"].get("key"): + 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 +141,6 @@ def verify(ocserv): else: raise ConfigError('openconnect network settings required') - def generate(ocserv): if not ocserv: return None @@ -132,6 +150,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 = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval", "30") + token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token-type", "hotp-time") + otp_length = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp-length", "6") + 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 +223,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..36d807a3c --- /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) |