2121from datetime import datetime
2222import boto3
2323from botocore .exceptions import ClientError
24- from ldap3 import Server , Connection , SASL , KERBEROS , Tls
24+ from ldap3 import Connection , SASL , KERBEROS
2525from ldap3 .core .rdns import ReverseDnsSetting
2626import dns .resolver
2727
2828"""
2929Constants
3030"""
31- KINIT_DELAY_IN_SECS = 60 * 45
31+ DEFAULT_KERBEROS_REFRESH = 60 * 45
3232KEYTAB_FILE_NAME = "krb5.keytab"
33+ CONF_FILE_NAME = "/etc/krb5.conf"
3334SECRET_ARN = "secret_arn"
3435DIRECTORY_NAME = "directory_name"
3536REGION_NAME = "region_name"
3839KRB_DIR = "krb_dir"
3940USERNAME_KEY = "username"
4041PASSWORD_KEY = "password"
41- MAX_FAILURES_IN_ABOUT_A_DAY = 24
42+ MAX_FAILURES = 24
43+ FAILURE_RETRY_PERIOD = "failure_retry_period"
44+ DEFAULT_FAILURE_RETRY_PERIOD = 30
4245
4346
4447def get_secret (region_name_arg , secret_arn_arg ):
@@ -50,8 +53,8 @@ def get_secret(region_name_arg, secret_arn_arg):
5053 :type region_name_arg: basestring such as "us-west-1"
5154 :param secret_arn_arg: Secret ARN for AWS Secrets Manager secret
5255 :type secret_arn_arg: basestring such as "arn:aws:secretsmanager:us-west-1...
53- :return: Secrets in string or None if there is an error
54- :rtype: basestring or none
56+ :return: username, password, domain in str or None if there is an error
57+ :rtype: tuple of 3 str or 3 None
5558 """
5659
5760 session = boto3 .session .Session ()
@@ -64,44 +67,60 @@ def get_secret(region_name_arg, secret_arn_arg):
6467 get_secret_value_response = client .get_secret_value (
6568 SecretId = secret_arn_arg
6669 )
70+ # Secrets Manager decrypts the secret value using the associated KMS CMK
71+ # Depending on whether the secret was a string or binary, only one of
72+ # these fields will be populated
73+ if 'SecretString' in get_secret_value_response :
74+ text_secret_data = get_secret_value_response ['SecretString' ]
75+ secret = text_secret_data
76+ else :
77+ binary_secret_data = get_secret_value_response ['SecretBinary' ]
78+ secret = binary_secret_data
79+
80+ try :
81+ secret_dict = json .loads (secret )
82+ except ValueError as _ :
83+ print ("ERROR* Secret doesn't contain valid JSON" , flush = True )
84+ try :
85+ username = secret_dict [USERNAME_KEY ]
86+ except KeyError as _ :
87+ print ("ERROR* Secret doesn't contain username" , flush = True )
88+ try :
89+ password = secret_dict [PASSWORD_KEY ]
90+ except KeyError as _ :
91+ print ("ERROR* Secret doesn't contain password" , flush = True )
92+ domain = secret_dict .get (DIRECTORY_NAME )
93+ # Missing values are handled in the caller
94+ return username , password , domain
6795 except ClientError as e :
6896 if e .response ['Error' ]['Code' ] == 'ResourceNotFoundException' :
6997 print ("The requested secret " + secret_arn_arg + " was not found" ,
7098 flush = True )
99+ # Retry this because the secret can be created later
100+ return None , None , None
71101 elif e .response ['Error' ]['Code' ] == 'InvalidRequestException' :
72102 print ("The request was invalid due to:" , e , flush = True )
103+ raise # there is no point to retry because there is nothing that can change
73104 elif e .response ['Error' ]['Code' ] == 'InvalidParameterException' :
74105 print ("The request had invalid params:" , e , flush = True )
106+ raise # there is no point to retry because there is nothing that can change
75107 elif e .response ['Error' ]['Code' ] == 'DecryptionFailure' :
76108 print (
77109 "The requested secret can't be decrypted using the provided KMS "
78110 "key:" ,
79111 e , flush = True )
112+ raise # there is no point to retry because there is nothing that can change
80113 elif e .response ['Error' ]['Code' ] == 'InternalServiceError' :
81114 print ("An error occurred on service side:" , e , flush = True )
82- else :
83- # Secrets Manager decrypts the secret value using the associated KMS CMK
84- # Depending on whether the secret was a string or binary, only one of
85- # these fields will be populated
86- if 'SecretString' in get_secret_value_response :
87- text_secret_data = get_secret_value_response ['SecretString' ]
88- secret = text_secret_data
89- else :
90- binary_secret_data = get_secret_value_response ['SecretBinary' ]
91- secret = binary_secret_data
92-
93- secret_string = json .loads (secret )
94- username = secret_string [USERNAME_KEY ]
95- password = secret_string [PASSWORD_KEY ]
96-
97- if username is None or password is None :
98- """
99- If Secrets Manager is not properly configured, the program will exit
100- """
101- print ("ERROR* Secret not available from Secrets Manager" , flush = True )
102- sys .exit (1 )
103-
104- return username , password
115+ # Retry this, the service can fix itself
116+ return None , None , None
117+ elif e .response ['Error' ]['Code' ] == 'AccessDeniedException' :
118+ print (f"Access denied when reading secret { secret_arn_arg } . Check your container execution role:" ,
119+ e , flush = True )
120+ # Retry this, they can fix the role without restarting
121+ return None , None , None
122+ # All other exceptions will be caught in the caller
123+ raise
105124
106125
107126def get_dc_server_names (directory_name_arg ):
@@ -209,6 +228,23 @@ def create_keytab(username_arg, password_arg, directory_name_arg,
209228 return
210229
211230
231+ def update_krb5_conf (directory_name_arg : str , krb5_conf_filename : str ) -> None :
232+ """
233+ Replaces realm and domain names in krb5.conf with the parameter
234+ Throws exception if keytab creation fails.
235+ :param directory_name_arg: Directory name of AD domain such as example.com
236+ :param krb5_conf_filename: file location of krb5.conf
237+ """
238+ with open (krb5_conf_filename , 'r' ) as krb5_conf_file :
239+ lines = krb5_conf_file .readlines ()
240+ for i , line in enumerate (lines ):
241+ lines [i ] = line .replace (
242+ "{REPLACE_WITH_DEFAULT_REALM}" , directory_name_arg .upper ()).replace (
243+ "{REPLACE_WITH_DOMAIN_NAME}" , directory_name_arg )
244+ with open (krb5_conf_filename , 'r' ) as krb5_conf_file :
245+ krb5_conf_file .writelines (lines )
246+
247+
212248def execute_kinit_cmd (username_arg , password_arg , directory_name_arg ):
213249 """
214250 Get kerberos tickets by executing the kinit command
@@ -250,16 +286,21 @@ def read_env():
250286 :return: Environment variables
251287 :rtype: Dictionary
252288 """
253- secret_arn = str (os .environ .get ('CREDENTIALS_SECRET_ARN' ))
254- directory_name = str (os .environ .get ('DOMAIN_NAME' ))
255- region_name = str (os .environ .get ('AWS_REGION' ))
256- service_principal_name = str (os .environ .get ('SERVICE_PRINCIPAL_NAME' ))
257- krb_ticket_refresh_period = os .environ .get (
258- "KRB_TICKET_REFRESH_PERIOD_IN_SECS" )
259- if krb_ticket_refresh_period is None or not isinstance (
260- krb_ticket_refresh_period , int ):
261- krb_ticket_refresh_period = KINIT_DELAY_IN_SECS
262- krb_dir = str (os .getenv ("KRB_DIR" ))
289+ secret_arn = os .environ .get ('CREDENTIALS_SECRET_ARN' )
290+ directory_name = os .environ .get ('DOMAIN_NAME' )
291+ region_name = os .environ .get ('AWS_REGION' )
292+ service_principal_name = os .environ .get ('SERVICE_PRINCIPAL_NAME' )
293+ try :
294+ krb_ticket_refresh_period = int (os .environ .get ("KRB_TICKET_REFRESH_PERIOD_IN_SECS" ))
295+ except (TypeError , ValueError ) as _ :
296+ print (f"Invalid value for env KRB_TICKET_REFRESH_PERIOD_IN_SECS, using default { DEFAULT_KERBEROS_REFRESH } " )
297+ krb_ticket_refresh_period = DEFAULT_KERBEROS_REFRESH
298+ try :
299+ failure_retry_period = int (os .environ .get ("FAILURE_RETRY_PERIOD" ))
300+ except (TypeError , ValueError ) as _ :
301+ print (f"Invalid value for env FAILURE_RETRY_PERIOD, using default { DEFAULT_FAILURE_RETRY_PERIOD } " )
302+ failure_retry_period = DEFAULT_FAILURE_RETRY_PERIOD
303+ krb_dir = os .getenv ("KRB_DIR" )
263304
264305 if not secret_arn or not directory_name or not region_name or not krb_dir \
265306 or not service_principal_name :
@@ -295,6 +336,7 @@ def read_env():
295336 REGION_NAME : region_name ,
296337 SERVICE_PRINCIPAL_NAME : service_principal_name ,
297338 KRB_TICKET_REFRESH_PERIOD : krb_ticket_refresh_period ,
339+ FAILURE_RETRY_PERIOD : failure_retry_period ,
298340 KRB_DIR : krb_dir }
299341 return env_vars
300342
@@ -339,7 +381,7 @@ def check_ldap_info(env_vars):
339381 print ("LDAP check done" , flush = True )
340382 ldap_check_status = True
341383 break
342- except :
384+ except Exception as _ :
343385 print ("LDAP check failed using DNS server = " + server , flush = True )
344386 continue
345387 if not ldap_check_status :
@@ -360,64 +402,49 @@ def main():
360402
361403 username = None
362404 password = None
363- for num_retries in range (5 ):
364- try :
365- username , password = get_secret (env_vars [REGION_NAME ],
366- env_vars [SECRET_ARN ])
367- break
368- except :
369- print ("[%s] ERROR** JSON error while loading secrets from secrets "
370- "manager" % num_retries ,
371- flush = True )
372- sys .exit (1 )
373-
374- if username is None or password is None :
375- """
376- If Secrets Manager is not properly configured, the program will exit
377- """
378- print ("ERROR** Secret not available from Secrets Manager" , flush = True )
379- sys .exit (1 )
380-
381- # AD Sanity check, these can be extended later
382- try :
383- execute_kinit_cmd (username , password , env_vars [DIRECTORY_NAME ])
384- check_ldap_info (env_vars )
385- except :
386- print ("Warning** LDAP access failed" )
405+ domain = None
387406
388407 """
389- Kerberos ticket refresh every KINIT_DELAY_IN_SECS
408+ Kerberos ticket refresh every DEFAULT_KERBEROS_REFRESH
390409 The grace period for Kerberos even if passwords change, is about an hour
391- KINIT_DELAY_IN_SECS is set to 45 minutes
410+ DEFAULT_KERBEROS_REFRESH is set to 45 minutes
392411 """
393412 keytab_filename = env_vars [KRB_DIR ] + "/" + KEYTAB_FILE_NAME
394413 num_failures = 0
395414 while True :
396- if num_failures > MAX_FAILURES_IN_ABOUT_A_DAY :
415+ if num_failures > MAX_FAILURES :
397416 print ("ERROR** Max failures reached, exiting" , flush = True )
398417 sys .exit (1 )
399418 try :
400- username_new , password_new = get_secret (env_vars [REGION_NAME ],
401- env_vars [SECRET_ARN ])
402-
403- execute_kinit_cmd (username_new , password_new , env_vars [DIRECTORY_NAME ])
404-
405- if not os .path .isfile (keytab_filename ):
406- create_keytab (username_new , password_new , env_vars [DIRECTORY_NAME ],
407- env_vars [SERVICE_PRINCIPAL_NAME ], keytab_filename )
408-
409- if username_new != username or password_new != password :
410- print (
411- "Credentials change detected at " + str (datetime .now ()) +
412- "creating a new keytab file" , flush = True )
413- if os .path .isfile (keytab_filename ):
414- os .remove (keytab_filename )
415- username = username_new
416- password = password_new
417- create_keytab (username , password , env_vars [DIRECTORY_NAME ],
418- env_vars [SERVICE_PRINCIPAL_NAME ], keytab_filename )
419- num_failures = 0
420- except :
419+ # get_secret returns None for username and/or password in cases where retry makes sense, like
420+ # secret not found, and returns None for username and password
421+ username_new , password_new , domain_new = get_secret (env_vars [REGION_NAME ], env_vars [SECRET_ARN ])
422+ print (f"Got username { username_new } password { password_new } and domain name { domain_new } from secret" )
423+
424+ if username_new is not None and password_new is not None :
425+ if domain_new is not None :
426+ env_vars [DIRECTORY_NAME ] = domain_new
427+ if domain != env_vars [DIRECTORY_NAME ]:
428+ domain = env_vars [DIRECTORY_NAME ]
429+ update_krb5_conf (CONF_FILE_NAME , env_vars [DIRECTORY_NAME ])
430+ execute_kinit_cmd (username_new , password_new , env_vars [DIRECTORY_NAME ])
431+
432+ # Only create keytab if service principal is set in env
433+ if env_vars [SERVICE_PRINCIPAL_NAME ] and (username_new != username or password_new != password ):
434+ print (
435+ "Credentials change detected at " + str (datetime .now ()) +
436+ "creating a new keytab file" , flush = True )
437+ if os .path .isfile (keytab_filename ):
438+ os .remove (keytab_filename )
439+ username = username_new
440+ password = password_new
441+ create_keytab (username , password , env_vars [DIRECTORY_NAME ],
442+ env_vars [SERVICE_PRINCIPAL_NAME ], keytab_filename )
443+ num_failures = 0
444+ time .sleep (env_vars [KRB_TICKET_REFRESH_PERIOD ])
445+ else :
446+ time .sleep (env_vars [FAILURE_RETRY_PERIOD ])
447+ except Exception as _ :
421448 num_failures = num_failures + 1
422449 print ("ERROR** JSON error while loading secrets from secrets manager" ,
423450 flush = True )
@@ -426,8 +453,7 @@ def main():
426453 traceback .print_exception (exc_type , exc_value , exc_traceback ,
427454 limit = 5 , file = sys .stdout )
428455 traceback .print_exc (limit = 5 , file = sys .stdout )
429-
430- time .sleep (env_vars [KRB_TICKET_REFRESH_PERIOD ])
456+ time .sleep (env_vars [FAILURE_RETRY_PERIOD ])
431457
432458
433459if __name__ == "__main__" :
0 commit comments