Skip to content

Commit 2750885

Browse files
committed
Making sidecar to fill krb5.conf from parameters, passing DOMAIN in secret, general fixes and cleanup
1 parent fea0c1b commit 2750885

File tree

2 files changed

+116
-91
lines changed

2 files changed

+116
-91
lines changed

Templates/kerberosSideCar/Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ COPY krb5.conf /etc/krb5.conf
1313

1414
# Side-car source code
1515
COPY krb_side_car.py /
16-
RUN chmod +x /krb_side_car.py
1716

1817
VOLUME ["/var/scratch"]
1918

@@ -29,7 +28,7 @@ ENV PYTHONUNBUFFERED=1
2928
ENV SERVICE_PRINCIPAL_NAME={REPLACE_WITH_SERVICE_PRINCIPAL_NAME_STRING}
3029

3130
#ENV KRB_DIR="Directory kerberos tickets and keytab are saved in this directory such as /var/scratch"
32-
ENV_KRB_DIR="/var/scratch"
31+
ENV KRB_DIR="/var/scratch"
3332
# this must be the same directory for default_ccache_name and default_keytab_name in krb5.conf.
3433
# This directory must also be shared with the app container"
3534

Templates/kerberosSideCar/krb_side_car.py

Lines changed: 115 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
from datetime import datetime
2222
import boto3
2323
from botocore.exceptions import ClientError
24-
from ldap3 import Server, Connection, SASL, KERBEROS, Tls
24+
from ldap3 import Connection, SASL, KERBEROS
2525
from ldap3.core.rdns import ReverseDnsSetting
2626
import dns.resolver
2727

2828
"""
2929
Constants
3030
"""
31-
KINIT_DELAY_IN_SECS = 60 * 45
31+
DEFAULT_KERBEROS_REFRESH = 60 * 45
3232
KEYTAB_FILE_NAME = "krb5.keytab"
33+
CONF_FILE_NAME = "/etc/krb5.conf"
3334
SECRET_ARN = "secret_arn"
3435
DIRECTORY_NAME = "directory_name"
3536
REGION_NAME = "region_name"
@@ -38,7 +39,9 @@
3839
KRB_DIR = "krb_dir"
3940
USERNAME_KEY = "username"
4041
PASSWORD_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

4447
def 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

107126
def 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+
212248
def 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

433459
if __name__ == "__main__":

0 commit comments

Comments
 (0)