@@ -8,14 +8,16 @@ import sys
8
8
import subprocess
9
9
import syslog
10
10
import signal
11
-
11
+ import re
12
12
import jinja2
13
13
from sonic_py_common import device_info
14
14
from swsscommon .swsscommon import ConfigDBConnector , DBConnector , Table
15
15
16
16
# FILE
17
17
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
18
18
PAM_AUTH_CONF_TEMPLATE = "/usr/share/sonic/templates/common-auth-sonic.j2"
19
+ PAM_PASSWORD_CONF = "/etc/pam.d/common-password"
20
+ PAM_PASSWORD_CONF_TEMPLATE = "/usr/share/sonic/templates/common-password.j2"
19
21
NSS_TACPLUS_CONF = "/etc/tacplus_nss.conf"
20
22
NSS_TACPLUS_CONF_TEMPLATE = "/usr/share/sonic/templates/tacplus_nss.conf.j2"
21
23
NSS_RADIUS_CONF = "/etc/radius_nss.conf"
@@ -24,6 +26,16 @@ PAM_RADIUS_AUTH_CONF_TEMPLATE = "/usr/share/sonic/templates/pam_radius_auth.conf
24
26
NSS_CONF = "/etc/nsswitch.conf"
25
27
ETC_PAMD_SSHD = "/etc/pam.d/sshd"
26
28
ETC_PAMD_LOGIN = "/etc/pam.d/login"
29
+ ETC_LOGIN_DEF = "/etc/login.defs"
30
+
31
+ # Linux login.def default values (password hardening disable)
32
+ LINUX_DEFAULT_PASS_MAX_DAYS = 99999
33
+ LINUX_DEFAULT_PASS_WARN_AGE = 7
34
+
35
+ ACCOUNT_NAME = 0 # index of account name
36
+ AGE_DICT = { 'MAX_DAYS' : {'REGEX_DAYS' : r'^PASS_MAX_DAYS[ \t]*(?P<max_days>\d*)' , 'DAYS' : 'max_days' , 'CHAGE_FLAG' : '-M ' },
37
+ 'WARN_DAYS' : {'REGEX_DAYS' : r'^PASS_WARN_AGE[ \t]*(?P<warn_days>\d*)' , 'DAYS' : 'warn_days' , 'CHAGE_FLAG' : '-W ' }
38
+ }
27
39
PAM_LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/pam_limits.j2"
28
40
LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/limits.conf.j2"
29
41
PAM_LIMITS_CONF = "/etc/pam.d/pam-limits-conf"
@@ -85,8 +97,10 @@ def run_cmd(cmd, log_err=True, raise_exception=False):
85
97
def is_true (val ):
86
98
if val == 'True' or val == 'true' :
87
99
return True
88
- else :
100
+ elif val == 'False' or val == 'false' :
89
101
return False
102
+ syslog .syslog (syslog .LOG_ERR , "Failed to get bool value, instead val= {}" .format (val ))
103
+ return False
90
104
91
105
92
106
def is_vlan_sub_interface (ifname ):
@@ -867,6 +881,189 @@ class AaaCfg(object):
867
881
.format (err .cmd , err .returncode , err .output ))
868
882
869
883
884
+ class PasswHardening (object ):
885
+ def __init__ (self ):
886
+ self .passw_policies_default = {}
887
+ self .passw_policies = {}
888
+
889
+ self .debug = False
890
+ self .trace = False
891
+
892
+ def load (self , policies_conf ):
893
+ for row in policies_conf :
894
+ self .passw_policies_update (row , policies_conf [row ], modify_conf = False )
895
+
896
+ self .modify_passw_conf_file ()
897
+
898
+ def passw_policies_update (self , key , data , modify_conf = True ):
899
+ syslog .syslog (syslog .LOG_DEBUG , "passw_policies_update - key: {}" .format (key ))
900
+ syslog .syslog (syslog .LOG_DEBUG , "passw_policies_update - data: {}" .format (data ))
901
+
902
+ if data == {}:
903
+ self .passw_policies = {}
904
+ else :
905
+ if 'reject_user_passw_match' in data :
906
+ data ['reject_user_passw_match' ] = is_true (data ['reject_user_passw_match' ])
907
+ if 'lower_class' in data :
908
+ data ['lower_class' ] = is_true (data ['lower_class' ])
909
+ if 'upper_class' in data :
910
+ data ['upper_class' ] = is_true (data ['upper_class' ])
911
+ if 'digits_class' in data :
912
+ data ['digits_class' ] = is_true (data ['digits_class' ])
913
+ if 'special_class' in data :
914
+ data ['special_class' ] = is_true (data ['special_class' ])
915
+
916
+ if key == 'POLICIES' :
917
+ self .passw_policies = data
918
+
919
+ if modify_conf :
920
+ self .modify_passw_conf_file ()
921
+
922
+ def modify_single_file_inplace (self , filename , operations = None ):
923
+ if operations :
924
+ cmd = "sed -i {0} {1}" .format (' -i ' .join (operations ), filename )
925
+ syslog .syslog (syslog .LOG_DEBUG , "modify_single_file_inplace: cmd - {}" .format (cmd ))
926
+ os .system (cmd )
927
+
928
+ def set_passw_hardening_policies (self , passw_policies ):
929
+ # Password Hardening flow
930
+ # When feature is enabled, the passw_policies from CONFIG_DB will be set in the pam files /etc/pam.d/common-password and /etc/login.def.
931
+ # When the feature is disabled, the files above will be generate with the linux default (without secured passw_policies).
932
+ syslog .syslog (syslog .LOG_DEBUG , "modify_conf_file: passw_policies - {}" .format (passw_policies ))
933
+
934
+ template_passwh_file = os .path .abspath (PAM_PASSWORD_CONF_TEMPLATE )
935
+ env = jinja2 .Environment (loader = jinja2 .FileSystemLoader ('/' ), trim_blocks = True )
936
+ env .filters ['sub' ] = sub
937
+ template_passwh = env .get_template (template_passwh_file )
938
+
939
+ # Render common-password file with passw hardening policies if any. Other render without them.
940
+ pam_passwh_conf = template_passwh .render (debug = self .debug , passw_policies = passw_policies )
941
+
942
+ # Use rename(), which is atomic (on the same fs) to avoid empty file
943
+ with open (PAM_PASSWORD_CONF + ".tmp" , 'w' ) as f :
944
+ f .write (pam_passwh_conf )
945
+ os .chmod (PAM_PASSWORD_CONF + ".tmp" , 0o644 )
946
+ os .rename (PAM_PASSWORD_CONF + ".tmp" , PAM_PASSWORD_CONF )
947
+
948
+ # Age policy
949
+ # When feature disabled or age policy disabled, expiry days policy should be as linux default, other, accoriding CONFIG_DB.
950
+ curr_expiration = LINUX_DEFAULT_PASS_MAX_DAYS
951
+ curr_expiration_warning = LINUX_DEFAULT_PASS_WARN_AGE
952
+
953
+ if passw_policies :
954
+ if 'state' in passw_policies :
955
+ if passw_policies ['state' ] == 'enabled' :
956
+ if 'expiration' in passw_policies :
957
+ if int (self .passw_policies ['expiration' ]) != 0 : # value '0' meaning age policy is disabled
958
+ # the logic is to modify the expiration time according the last updated modificatiion
959
+ #
960
+ curr_expiration = int (passw_policies ['expiration' ])
961
+
962
+ if 'expiration_warning' in passw_policies :
963
+ if int (self .passw_policies ['expiration_warning' ]) != 0 : # value '0' meaning age policy is disabled
964
+ curr_expiration_warning = int (passw_policies ['expiration_warning' ])
965
+
966
+ if self .is_passwd_aging_expire_update (curr_expiration , 'MAX_DAYS' ):
967
+ # Set aging policy for existing users
968
+ self .passwd_aging_expire_modify (curr_expiration , 'MAX_DAYS' )
969
+
970
+ # Aging policy for new users
971
+ self .modify_single_file_inplace (ETC_LOGIN_DEF , ["\' /^PASS_MAX_DAYS/c\PASS_MAX_DAYS " + str (curr_expiration )+ "\' " ])
972
+
973
+ if self .is_passwd_aging_expire_update (curr_expiration_warning , 'WARN_DAYS' ):
974
+ # Aging policy for existing users
975
+ self .passwd_aging_expire_modify (curr_expiration_warning , 'WARN_DAYS' )
976
+
977
+ # Aging policy for new users
978
+ self .modify_single_file_inplace (ETC_LOGIN_DEF , ["\' /^PASS_WARN_AGE/c\PASS_WARN_AGE " + str (curr_expiration_warning )+ "\' " ])
979
+
980
+ def passwd_aging_expire_modify (self , curr_expiration , age_type ):
981
+ normal_accounts = self .get_normal_accounts ()
982
+ if not normal_accounts :
983
+ syslog .syslog (syslog .LOG_ERR ,"failed, no normal users found in /etc/passwd" )
984
+ return
985
+ chage_flag = AGE_DICT [age_type ]['CHAGE_FLAG' ]
986
+ for normal_account in normal_accounts :
987
+ try :
988
+ chage_p_m = subprocess .Popen (('chage' , chage_flag + str (curr_expiration ), normal_account ), stdout = subprocess .PIPE )
989
+ return_code_chage_p_m = chage_p_m .poll ()
990
+ if return_code_chage_p_m != 0 :
991
+ syslog .syslog (syslog .LOG_ERR , "failed: return code - {}" .format (return_code_chage_p_m ))
992
+
993
+ except subprocess .CalledProcessError as e :
994
+ syslog .syslog (syslog .LOG_ERR , "{} - failed: return code - {}, output:\n {}" .format (e .cmd , e .returncode , e .output ))
995
+
996
+ def is_passwd_aging_expire_update (self , curr_expiration , age_type ):
997
+ """ Function verify that the current age expiry policy values are equal from the old one
998
+ Return update_age_status 'True' value meaning that was a modification from the last time, and vice versa.
999
+ """
1000
+ update_age_status = False
1001
+ days_num = None
1002
+ regex_days = AGE_DICT [age_type ]['REGEX_DAYS' ]
1003
+ days_type = AGE_DICT [age_type ]['DAYS' ]
1004
+ if os .path .exists (ETC_LOGIN_DEF ):
1005
+ with open (ETC_LOGIN_DEF , 'r' ) as f :
1006
+ login_def_data = f .readlines ()
1007
+
1008
+ for line in login_def_data :
1009
+ m1 = re .match (regex_days , line )
1010
+ if m1 :
1011
+ days_num = int (m1 .group (days_type ))
1012
+ break
1013
+
1014
+ if curr_expiration != days_num :
1015
+ update_age_status = True
1016
+
1017
+ return update_age_status
1018
+
1019
+ def get_normal_accounts (self ):
1020
+ # Get user list
1021
+ try :
1022
+ getent_out = subprocess .check_output (['getent' , 'passwd' ]).decode ('utf-8' ).split ('\n ' )
1023
+ except subprocess .CalledProcessError as err :
1024
+ syslog .syslog (syslog .LOG_ERR , "{} - failed: return code - {}, output:\n {}" .format (err .cmd , err .returncode , err .output ))
1025
+ return False
1026
+
1027
+ # Get range of normal users
1028
+ REGEX_UID_MAX = r'^UID_MAX[ \t]*(?P<uid_max>\d*)'
1029
+ REGEX_UID_MIN = r'^UID_MIN[ \t]*(?P<uid_min>\d*)'
1030
+ uid_max = None
1031
+ uid_min = None
1032
+ if os .path .exists (ETC_LOGIN_DEF ):
1033
+ with open (ETC_LOGIN_DEF , 'r' ) as f :
1034
+ login_def_data = f .readlines ()
1035
+
1036
+ for line in login_def_data :
1037
+ m1 = re .match (REGEX_UID_MAX , line )
1038
+ m2 = re .match (REGEX_UID_MIN , line )
1039
+ if m1 :
1040
+ uid_max = int (m1 .group ("uid_max" ))
1041
+ if m2 :
1042
+ uid_min = int (m2 .group ("uid_min" ))
1043
+
1044
+ if not uid_max or not uid_min :
1045
+ syslog .syslog (syslog .LOG_ERR ,"failed, no UID_MAX/UID_MIN founded in login.def file" )
1046
+ return False
1047
+
1048
+ # Get normal user list
1049
+ normal_accounts = []
1050
+ for account in getent_out [0 :- 1 ]: # last item is always empty
1051
+ account_spl = account .split (':' )
1052
+ account_number = int (account_spl [2 ])
1053
+ if account_number >= uid_min and account_number <= uid_max :
1054
+ normal_accounts .append (account_spl [ACCOUNT_NAME ])
1055
+
1056
+ normal_accounts .append ('root' ) # root is also a candidate to be age modify.
1057
+ return normal_accounts
1058
+
1059
+ def modify_passw_conf_file (self ):
1060
+ passw_policies = self .passw_policies_default .copy ()
1061
+ passw_policies .update (self .passw_policies )
1062
+
1063
+ # set new Password Hardening policies.
1064
+ self .set_passw_hardening_policies (passw_policies )
1065
+
1066
+
870
1067
class KdumpCfg (object ):
871
1068
def __init__ (self , CfgDb ):
872
1069
self .config_db = CfgDb
@@ -1090,6 +1287,9 @@ class HostConfigDaemon:
1090
1287
self .hostname_cache = ""
1091
1288
self .aaacfg = AaaCfg ()
1092
1289
1290
+ # Initialize PasswHardening
1291
+ self .passwcfg = PasswHardening ()
1292
+
1093
1293
# Initialize PamLimitsCfg
1094
1294
self .pamLimitsCfg = PamLimitsCfg (self .config_db )
1095
1295
self .pamLimitsCfg .update_config_file ()
@@ -1105,12 +1305,14 @@ class HostConfigDaemon:
1105
1305
ntp_server = init_data ['NTP_SERVER' ]
1106
1306
ntp_global = init_data ['NTP' ]
1107
1307
kdump = init_data ['KDUMP' ]
1308
+ passwh = init_data ['PASSW_HARDENING' ]
1108
1309
1109
1310
self .feature_handler .sync_state_field (features )
1110
1311
self .aaacfg .load (aaa , tacacs_global , tacacs_server , radius_global , radius_server )
1111
1312
self .iptables .load (lpbk_table )
1112
1313
self .ntpcfg .load (ntp_global , ntp_server )
1113
1314
self .kdumpCfg .load (kdump )
1315
+ self .passwcfg .load (passwh )
1114
1316
1115
1317
dev_meta = self .config_db .get_table ('DEVICE_METADATA' )
1116
1318
if 'localhost' in dev_meta :
@@ -1131,6 +1333,10 @@ class HostConfigDaemon:
1131
1333
self .aaacfg .aaa_update (key , data )
1132
1334
syslog .syslog (syslog .LOG_INFO , 'AAA Update: key: {}, op: {}, data: {}' .format (key , op , data ))
1133
1335
1336
+ def passwh_handler (self , key , op , data ):
1337
+ self .passwcfg .passw_policies_update (key , data )
1338
+ syslog .syslog (syslog .LOG_INFO , 'PASSW_HARDENING Update: key: {}, op: {}, data: {}' .format (key , op , data ))
1339
+
1134
1340
def tacacs_server_handler (self , key , op , data ):
1135
1341
self .aaacfg .tacacs_server_update (key , data )
1136
1342
log_data = copy .deepcopy (data )
@@ -1229,6 +1435,7 @@ class HostConfigDaemon:
1229
1435
self .config_db .subscribe ('TACPLUS_SERVER' , make_callback (self .tacacs_server_handler ))
1230
1436
self .config_db .subscribe ('RADIUS' , make_callback (self .radius_global_handler ))
1231
1437
self .config_db .subscribe ('RADIUS_SERVER' , make_callback (self .radius_server_handler ))
1438
+ self .config_db .subscribe ('PASSW_HARDENING' , make_callback (self .passwh_handler ))
1232
1439
# Handle IPTables configuration
1233
1440
self .config_db .subscribe ('LOOPBACK_INTERFACE' , make_callback (self .lpbk_handler ))
1234
1441
# Handle NTP & NTP_SERVER updates
0 commit comments