summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--debian/control1
-rwxr-xr-xdebian/rules4
-rw-r--r--interface-definitions/system-login-radius.xml.in67
-rw-r--r--interface-definitions/system-login-user.xml.in121
-rwxr-xr-xsrc/conf_mode/system-login-radius.py163
-rwxr-xr-xsrc/conf_mode/system-login-user.py221
-rwxr-xr-xsrc/migration-scripts/system/14-to-151
-rwxr-xr-xsrc/migration-scripts/system/15-to-1655
-rw-r--r--src/pam-configs/radius14
9 files changed, 646 insertions, 1 deletions
diff --git a/debian/control b/debian/control
index bca066aff..b80d6d557 100644
--- a/debian/control
+++ b/debian/control
@@ -63,6 +63,7 @@ Depends: python3,
openvpn,
openvpn-auth-ldap,
openvpn-auth-radius,
+ libpam-radius-auth (>= 1.5.0),
mtr-tiny,
telnet,
traceroute,
diff --git a/debian/rules b/debian/rules
index d529c9b4e..144132389 100755
--- a/debian/rules
+++ b/debian/rules
@@ -74,6 +74,10 @@ override_dh_auto_install:
mkdir -p $(DIR)/etc
cp -r src/etc/* $(DIR)/etc
+ # Install PAM configuration snippets
+ mkdir -p $(DIR)/usr/share/pam-configs
+ cp -r src/pam-configs/* $(DIR)/usr/share/pam-configs
+
# Install systemd service units
mkdir -p $(DIR)/lib/systemd/system
cp -r src/systemd/* $(DIR)/lib/systemd/system
diff --git a/interface-definitions/system-login-radius.xml.in b/interface-definitions/system-login-radius.xml.in
new file mode 100644
index 000000000..00e85db3e
--- /dev/null
+++ b/interface-definitions/system-login-radius.xml.in
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="login">
+ <children>
+ <node name="radius" owner="${vyos_conf_scripts_dir}/system-login-radius.py">
+ <properties>
+ <help>RADIUS based user authentication</help>
+ </properties>
+ <children>
+ <leafNode name="source-address">
+ <properties>
+ <help>RADIUS client source address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>TFTP IPv4 listen address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="server">
+ <properties>
+ <help>RADIUS server configuration</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>RADIUS shared secret key</help>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>RADIUS authentication port</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port (default: 1812)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="timeout">
+ <properties>
+ <help>Timeout for RADIUS session</help>
+ <valueHelp>
+ <format>1-30</format>
+ <description>Session timeout in seconds (default: 2)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-30"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/system-login-user.xml.in b/interface-definitions/system-login-user.xml.in
new file mode 100644
index 000000000..970bcf799
--- /dev/null
+++ b/interface-definitions/system-login-user.xml.in
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="system">
+ <children>
+ <node name="login">
+ <properties>
+ <help>User Login</help>
+ <priority>400</priority>
+ </properties>
+ <children>
+ <tagNode name="user" owner="${vyos_conf_scripts_dir}/system-login-user.py">
+ <properties>
+ <help>User account information</help>
+ <constraint>
+ <regex>[a-zA-Z0-9\-_\.]{1,100}</regex>
+ </constraint>
+ <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
+ </properties>
+ <children>
+ <node name="authentication">
+ <properties>
+ <help>Password authentication</help>
+ </properties>
+ <children>
+ <leafNode name="encrypted-password">
+ <properties>
+ <help>Encrypted password</help>
+ <constraint>
+ <regex>(\*|\!)</regex>
+ <regex>[a-zA-Z0-9\.\/]{13}$</regex>
+ <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
+ <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
+ <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
+
+ </properties>
+ </leafNode>
+ <leafNode name="plaintext-password">
+ <properties>
+ <help>Plaintext password used for encryption</help>
+ </properties>
+ </leafNode>
+ <tagNode name="public-keys">
+ <properties>
+ <help>Remote access public keys</help>
+ <valueHelp>
+ <format>&gt;identifier&lt;</format>
+ <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Public key value (base64-encoded)</help>
+ <completionHelp>
+ <script>echo 'The key is usually several hundred bytes long (because of the size of the public key encoding). Use the loadkey tool to input key from a URL or file.'</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="options">
+ <properties>
+ <help>Optional public key options</help>
+ </properties>
+ </leafNode>
+ <leafNode name="type">
+ <properties>
+ <help></help>
+ <completionHelp>
+ <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ssh-dss</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ssh-rsa</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ecdsa-sha2-nistp256</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ecdsa-sha2-nistp384</format>
+ <description/>
+ </valueHelp>
+ <valueHelp>
+ <format>ssh-ed25519</format>
+ <description/>
+ </valueHelp>
+ <constraint>
+ <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519s)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="full-name">
+ <properties>
+ <help>Full name of the user (use quotes for names with spaces)</help>
+ <constraint>
+ <regex>[^:]*$</regex>
+ </constraint>
+ <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="home-directory">
+ <properties>
+ <help>Home directory</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/system-login-radius.py b/src/conf_mode/system-login-radius.py
new file mode 100755
index 000000000..52010b6ea
--- /dev/null
+++ b/src/conf_mode/system-login-radius.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+import sys
+import os
+import jinja2
+
+from pwd import getpwall, getpwnam
+from stat import S_IRUSR, S_IWUSR
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos import ConfigError
+
+radius_config_file = "/etc/pam_radius_auth.conf"
+radius_config_tmpl = """
+# Automatically generated by VyOS
+# RADIUS configuration file
+# server[:port] shared_secret timeout (s) source_ip
+{% if server -%}
+{% for s in server -%}
+{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if source_address -%}{{ source_address }}{% endif %}
+{% endfor -%}
+
+priv-lvl 15
+mapped_priv_user radius_priv_user
+{% endif %}
+
+"""
+
+default_config_data = {
+ 'server': [],
+ 'source_address': '',
+}
+
+def get_local_users():
+ """Returns list of dynamically allocated users (see Debian Policy Manual)"""
+ local_users = []
+ for p in getpwall():
+ username = p[0]
+ uid = getpwnam(username).pw_uid
+ if uid in range(1000, 29999):
+ if username not in ['radius_user', 'radius_priv_user']:
+ local_users.append(username)
+
+ return local_users
+
+def get_config():
+ radius = default_config_data
+ conf = Config()
+ base_level = ['system', 'login', 'radius']
+
+ if not conf.exists(base_level):
+ return radius
+
+ conf.set_level(base_level)
+
+ if conf.exists(['source-address']):
+ radius['source_address'] = conf.return_value(['source-address'])
+
+ # Read in all RADIUS servers and store to list
+ for server in conf.list_nodes(['server']):
+ server_cfg = {
+ 'address': server,
+ 'key': '',
+ 'port': '1812',
+ 'timeout': '2'
+ }
+ conf.set_level(base_level + ['server', server])
+
+ # RADIUS shared secret
+ if conf.exists(['key']):
+ server_cfg['key'] = conf.return_value(['key'])
+
+ # RADIUS authentication port
+ if conf.exists(['port']):
+ server_cfg['port'] = conf.return_value(['port'])
+
+ # RADIUS session timeout
+ if conf.exists(['timeout']):
+ server_cfg['timeout'] = conf.return_value(['timeout'])
+
+ # Append individual RADIUS server configuration to global server list
+ radius['server'].append(server_cfg)
+
+ return radius
+
+def verify(radius):
+ pass
+
+def generate(radius):
+ if len(radius['server']) > 0:
+ tmpl = jinja2.Template(radius_config_tmpl)
+ config_text = tmpl.render(radius)
+ with open(radius_config_file, 'w') as f:
+ f.write(config_text)
+
+ uid = getpwnam('root').pw_uid
+ gid = getpwnam('root').pw_gid
+ os.chown(radius_config_file, uid, gid)
+ os.chmod(radius_config_file, S_IRUSR | S_IWUSR)
+ else:
+ os.unlink(radius_config_file)
+
+ return None
+
+def apply(radius):
+ if len(radius['server']) > 0:
+ try:
+ # Enable RADIUS in PAM
+ os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius")
+
+ # Make NSS system aware of RADIUS, too
+ cmd = "sed -i -e \'/\smapname/b\' \
+ -e \'/^passwd:/s/\s\s*/&mapuid /\' \
+ -e \'/^passwd:.*#/s/#.*/mapname &/\' \
+ -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
+ -e \'/^group:.*#/s/#.*/ mapname &/\' \
+ -e \'/^group:[^#]*$/s/: */&mapname /\' \
+ /etc/nsswitch.conf"
+
+ os.system(cmd)
+ except:
+ print('RADIUS configuration failed')
+ else:
+ try:
+ # Disable RADIUS in PAM
+ os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius")
+
+ cmd = "'sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
+ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
+ -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
+ -e \'s/[ \t]*$//\' \
+ /etc/nsswitch.conf"
+
+ os.system(cmd)
+ except:
+ print('Removing RADIUS configuration failed')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/system-login-user.py b/src/conf_mode/system-login-user.py
new file mode 100755
index 000000000..a9e194859
--- /dev/null
+++ b/src/conf_mode/system-login-user.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+import sys
+import os
+
+from pwd import getpwall, getpwnam
+from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP
+from subprocess import Popen, PIPE, STDOUT
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos import ConfigError
+
+default_config_data = {
+ 'deleted': False,
+ 'add_users': [],
+ 'del_users': []
+}
+
+def get_local_users():
+ """Returns list of dynamically allocated users (see Debian Policy Manual)"""
+ local_users = []
+ for p in getpwall():
+ username = p[0]
+ uid = getpwnam(username).pw_uid
+ if uid in range(1000, 29999):
+ if username not in ['radius_user', 'radius_priv_user']:
+ local_users.append(username)
+
+ return local_users
+
+
+def get_crypt_pw(password):
+ command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password)
+ p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
+ tmp = p.communicate()[0].strip()
+ return tmp.decode()
+
+
+def get_config():
+ login = default_config_data
+ conf = Config()
+ base_level = ['system', 'login', 'user']
+
+ if not conf.exists(base_level):
+ login['deleted'] = True
+ return login
+
+ # Read in all local users and store to list
+ for username in conf.list_nodes(base_level):
+ user = {
+ 'name': username,
+ 'password_plaintext': '',
+ 'password_encrypted': '!',
+ 'public_keys': [],
+ 'full_name': '',
+ 'home_dir': '/home/' + username,
+ }
+ conf.set_level(base_level + [username])
+
+ # Plaintext password
+ if conf.exists(['authentication', 'plaintext-password']):
+ user['password_plaintext'] = conf.return_value(['authentication', 'plaintext-password'])
+
+ # Encrypted password
+ if conf.exists(['authentication', 'encrypted-password']):
+ user['password_encrypted'] = conf.return_value(['authentication', 'encrypted-password'])
+
+ # User real name
+ if conf.exists(['full-name']):
+ user['full_name'] = conf.return_value(['full-name'])
+
+ # User home-directory
+ if conf.exists(['home-directory']):
+ user['home_dir'] = conf.return_value(['home-directory'])
+
+ # Read in public keys
+ for id in conf.list_nodes(['authentication', 'public-keys']):
+ key = {
+ 'name': id,
+ 'key': '',
+ 'options': '',
+ 'type': ''
+ }
+ conf.set_level(base_level + [username, 'authentication', 'public-keys', id])
+
+ # Public Key portion
+ if conf.exists(['key']):
+ key['key'] = conf.return_value(['key'])
+
+ # Options for individual public key
+ if conf.exists(['options']):
+ key['options'] = conf.return_value(['options'])
+
+ # Type of public key
+ if conf.exists(['type']):
+ key['type'] = conf.return_value(['type'])
+
+ # Append individual public key to list of user keys
+ user['public_keys'].append(key)
+
+ login['add_users'].append(user)
+
+ return login
+
+def verify(login):
+ # TODO: should we be able to delete ourself?
+ pass
+
+def generate(login):
+ # users no longer existing in the running configuration need to be deleted
+ local_users = get_local_users()
+ cli_users = [tmp['name'] for tmp in login['add_users']]
+ # create a list of all users, cli and users
+ all_users = list(set(local_users+cli_users))
+
+ # Remove any normal users that dos not exist in the current configuration.
+ # This can happen if user is added but configuration was not saved and
+ # system is rebooted.
+ login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users]
+
+ # calculate users encrypted password
+ for user in login['add_users']:
+ if user['password_plaintext']:
+ user['password_encrypted'] = get_crypt_pw(user['password_plaintext'])
+ user['password_plaintext'] = ''
+
+ # remove old plaintext password
+ # and set new encrypted password
+ os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name']))
+ os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted']))
+
+ return None
+
+def apply(login):
+ for user in login['add_users']:
+ # make new user using vyatta shell and make home directory (-m),
+ # default group of 100 (users)
+ cmd = "useradd -m -N"
+ # check if user already exists:
+ if user['name'] in get_local_users():
+ # update existing account
+ cmd = "usermod"
+
+ # encrypted password must be quited in '' else it won't work!
+ cmd += ' -p "{}"'.format(user['password_encrypted'])
+ cmd += ' -s /bin/vbash'
+ if user['full_name']:
+ cmd += ' -c "{}"'.format(user['full_name'])
+
+ if user['home_dir']:
+ cmd += ' -d "{}"'.format(user['home_dir'])
+
+ cmd += ' -G frrvty,vyattacfg,sudo,adm,dip,disk'
+ cmd += ' {}'.format(user['name'])
+
+ try:
+ os.system(cmd)
+
+ uid = getpwnam(user['name']).pw_uid
+ gid = getpwnam(user['name']).pw_gid
+
+ # install ssh keys
+ key_dir = '{}/.ssh'.format(user['home_dir'])
+ if not os.path.isdir(key_dir):
+ os.mkdir(key_dir)
+ os.chown(key_dir, uid, gid)
+ os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
+
+ key_file = key_dir + '/authorized_keys';
+ with open(key_file, 'w') as f:
+ f.write("# Automatically generated by VyOS\n")
+ f.write("# Do not edit, all changes will be lost\n")
+
+ for id in user['public_keys']:
+ line = ''
+ if id['options']:
+ line = '{} '.format(id['options'])
+
+ line += '{} {} {}\n'.format(id['type'], id['key'], id['name'])
+ f.write(line)
+
+ os.chown(key_file, uid, gid)
+ os.chmod(key_file, S_IRUSR | S_IWUSR)
+
+ except Exception as e:
+ print('Adding user "{}" raised an exception: {}'.format(user['name'], e))
+
+ for user in login['del_users']:
+ try:
+ # TODO: check if user is logged in and force logout
+ # Remove user account but leave home directory to be safe
+ os.system('userdel {}'.format(user))
+ except Exception as e:
+ print('Deleting user "{}" raised an exception'.format(user))
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/migration-scripts/system/14-to-15 b/src/migration-scripts/system/14-to-15
index fd89ae57a..2491e3d0d 100755
--- a/src/migration-scripts/system/14-to-15
+++ b/src/migration-scripts/system/14-to-15
@@ -22,7 +22,6 @@ if not config.exists(base):
# Nothing to do
sys.exit(0)
else:
- # delete 'system ipv6 blacklist' node
if config.exists(base + ['reboot-on-panic']):
reboot = config.return_value(base + ['reboot-on-panic'])
config.delete(base + ['reboot-on-panic'])
diff --git a/src/migration-scripts/system/15-to-16 b/src/migration-scripts/system/15-to-16
new file mode 100755
index 000000000..e70893d55
--- /dev/null
+++ b/src/migration-scripts/system/15-to-16
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+# * remove "system login user <user> group" node, Why should be add a user to a
+# 3rd party group when the system is fully managed by CLI?
+# * remove "system login user <user> level" node
+# This is the only privilege level left and also the default, what is the
+# sense in keeping this orphaned node?
+
+import os
+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)
+base = ['system', 'login', 'user']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ for user in config.list_nodes(base):
+ if config.exists(base + [user, 'group']):
+ config.delete(base + [user, 'group'])
+
+ if config.exists(base + [user, 'level']):
+ config.delete(base + [user, 'level'])
+
+ 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)
diff --git a/src/pam-configs/radius b/src/pam-configs/radius
new file mode 100644
index 000000000..9353de458
--- /dev/null
+++ b/src/pam-configs/radius
@@ -0,0 +1,14 @@
+Name: RADIUS authentication
+Default: no
+Priority: 257
+Auth-Type: Primary
+Auth:
+ [authinfo_unavail=ignore success=end auth_err=bad default=ignore] pam_radius_auth.so
+
+Account-Type: Primary
+Account:
+ [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so
+
+Session-Type: Additional
+Session:
+ [authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so