diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..602ce94a Binary files /dev/null and b/.DS_Store differ diff --git a/CMakeLists.txt b/CMakeLists.txt index ec663bdb..dbd29eea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,36 +6,30 @@ include(GNUInstallDirs) set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) set(CMAKE_CXX_FLAGS " -std=c++17 -g -Wall -Werror -pthread") +#These commands are not supported in CMake available for Amazon Linux 2 +#cmake_host_system_information(RESULT PRETTY_NAME QUERY DISTRIB_PRETTY_NAME) +#message(STATUS "${PRETTY_NAME}") +# +#cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO) -set(protobuf_MODULE_COMPATIBLE TRUE) -find_package(Protobuf REQUIRED) -find_package(gRPC REQUIRED) -set(_PROTOBUF_LIBPROTOBUF protobuf::libprotobuf) -set(_REFLECTION gRPC::grpc++_reflection) -find_program(_PROTOBUF_PROTOC protoc) -find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin) - -cmake_host_system_information(RESULT PRETTY_NAME QUERY DISTRIB_PRETTY_NAME) -message(STATUS "${PRETTY_NAME}") - -cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO) - -foreach(VAR IN LISTS DISTRO) - message(STATUS "${VAR}=`${${VAR}}`") -endforeach() +#foreach(VAR IN LISTS DISTRO) +# message(STATUS "${VAR}=`${${VAR}}`") +#endforeach() set(CMAKE_VERBOSE_MAKEFILE ON) set(config) add_subdirectory(config) -set(api) -add_subdirectory(api) set(renewal) add_subdirectory(renewal) -set(daemon) -add_subdirectory(daemon) set(metadata) add_subdirectory(metadata) +set(auth) +add_subdirectory(auth) +set(daemon) +add_subdirectory(daemon) +set(sample_credspec) +add_subdirectory(sample_credspec) if (NOT CF_KRB_DIR) set(CF_KRB_DIR "/var/credentials-fetcher/krbdir") @@ -97,7 +91,7 @@ else() ) endif() -set(sources ${daemon} ${config} ${renewal}) +set(sources ${daemon} ${config} ${renewal} ${auth} ${metadata}) add_executable(credentials-fetcherd ${sources}) @@ -107,19 +101,21 @@ check_pie_supported() if (CMAKE_C_LINK_PIE_SUPPORTED) set_property(TARGET credentials-fetcherd PROPERTY POSITION_INDEPENDENT_CODE TRUE) - set_property(TARGET cf_gmsa_service_private - PROPERTY POSITION_INDEPENDENT_CODE TRUE) endif () find_path(GLIB_INCLUDE_DIR glib.h "/usr/include" "/usr/include/glib-2.0") find_path(GLIB_CONFIG_DIR glibconfig.h "/usr/include" "/usr/lib64/glib-2.0/include" "/usr/lib/x86_64-linux-gnu/glib-2.0/include/") +set(BOOST_FILESYSTEM_H "/usr/include/boost/filesystem") +set(KRB5_CLIENT_H ${CMAKE_CURRENT_SOURCE_DIR}/auth/kinit_client/) target_include_directories(credentials-fetcherd PUBLIC common ${GLIB_INCLUDE_DIR} ${GLIB_CONFIG_DIR} - ${CMAKE_CURRENT_BINARY_DIR}) + ${BOOST_FILESYSTEM_H} + ${CMAKE_CURRENT_BINARY_DIR} + ${KRB5_CLIENT_H}) find_program(DOTNET dotnet ~/.dotnet /usr/bin) if (NOT DOTNET) @@ -137,14 +133,16 @@ target_include_directories(credentials-fetcherd PUBLIC common) if(${DISTRO_ID} MATCHES "ubuntu") message(STATUS "Linux distro detected as ubuntu") target_link_libraries(credentials-fetcherd - PUBLIC systemd krb5 glib-2.0 cf_gmsa_service_private - crypto protobuf kadm5srv_mit kdb5 gssrpc gssapi_krb5 gssrpc k5crypto com_err krb5support resolv utf8_validity) + PUBLIC systemd krb5 glib-2.0 + crypto protobuf kadm5srv_mit kdb5 gssrpc gssapi_krb5 gssrpc k5crypto com_err krb5support resolv utf8_validity jsoncpp) else() + target_link_libraries(credentials-fetcherd - PUBLIC systemd krb5 glib-2.0 cf_gmsa_service_private + PUBLIC systemd krb5 glib-2.0 crypto + -L/usr/local/lib kadm5srv_mit kdb5 gssrpc gssapi_krb5 gssrpc k5crypto - com_err krb5support resolv) + com_err krb5support resolv jsoncpp) endif() install(FILES ${CMAKE_BINARY_DIR}/credentials-fetcherd diff --git a/README-NOGRPC.MD b/README-NOGRPC.MD new file mode 100644 index 00000000..97c2ffc2 --- /dev/null +++ b/README-NOGRPC.MD @@ -0,0 +1,140 @@ +"No gRPC Mode" is intended to be used for this scenario: +* Neeeds to run on Amazon Linux 2 which does not have packages available for gRPC +* EKS host not ECS +* Need to run via command line and not configured via gRPC api calls +* Containers do not need access to kerbos tickets + +#compile no_grpc_mode on AmazonLinux2 + +``` + sudo yum install cmake3 + sudo yum install git -y + sudo yum install gcc10-c++ -y + sudo mv /usr/bin/gcc /usr/bin/gcc-7.3 + sudo ln -s /usr/bin/gcc10-cc /usr/bin/gcc + sudo mv /usr/bin/g++ /usr/bin/g++-7.3 + sudo ln -s /usr/bin/gcc10-c++ /usr/bin/g++ + sudo ln -s /usr/bin/gcc10-c++ /usr/bin/g++ + sudo mv /usr/bin/c++ /usr/bin/c++-7.3 + sudo ln -s /usr/bin/gcc10-c++ /usr/bin/c++ + sudo ln -s /usr/bin/gcc10-c++ /usr/bin/c++ + sudo yum install openssl-devel -y + sudo yum install openssl-devel -y +``` + + #install DotNet 6 +``` + sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm + sudo yum install aspnetcore-runtime-6.0 -y + sudo yum install dotnet-sdk-6.0 -y + cd .. +``` + #Install packages need by Credentials-Fetcher +``` + sudo yum install glib* -y + sudo yum install jsoncpp-devel jsoncpp -y + sudo yum install systemd-devel -y + ``` + + #build Credentials-Fetcher +``` + + mkdir build + cd build + + cmake3 ../ + make -j 4 +``` + +#Copy helper app to /usr/sbin +``` + sudo cp credentials_fetcher_utf16_private.exe /usr/sbin/ + sudo cp credentials_fetcher_utf16_private.runtimeconfig.json /usr/sbin/ + ``` + + #install prereqs to run +``` + sudo yum install unzip + sudo yum install realmd -y + sudo yum install which -y + sudo yum install hostname -y + sudo yum install krb5-workstation -y + sudo yum install openldap -y + sudo yum install openldap-clients -y + sudo yum install cyrus-sasl-gssapi -y + + #install AWS CLI used to read AWS secrets + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + +``` + +#set environment variables need to run in CLI mode +``` + export CF_CRED_SPEC_FILE=/home/ec2-user/credentials-fetcher/no-grpc-mode/build/credspec.json + #todo:make sure EC2 Instance Profile has permissions to AWS Secrets Manager + + #configure AWS region to your region + aws configure set default.region us-east-1 +``` + +#run it in domainless mode by specific the AWS SecretsManger secret +#Replace gmsa-plugin-input with your AWS SecretsManager secret name. + +``` + ./credentials-fetcherd --aws_sm_secret_name gmsa-plugin-input +``` + +#You should see output similar to this +``` +[root@ip-10-0-61-81 build]# ./credentials-fetcherd --aws_sm_secret_name gmsa-plugin-input +Option selected for domainless operation, AWS secrets manager secret-name = gmsa-plugin-input +krb_files_dir = /var/credentials-fetcher/krbdir +cred_file = /home/ec2-user/credentials-fetcher/no-grpc-mode/build/credspec.json (lease id: credspec) +logging_dir = /var/credentials-fetcher/logging +unix_socket_dir = /var/credentials-fetcher/socket +Using existing cache: persistent:0:0 +Using principal: eks-portable-ident@EXAMPLE.COM +prompt at 0x970ea702, 0x400, '�� +prompt at 0x970ea702, 0x400, '�� +Authenticated to Kerberos v5 +ldapsearch -H ldap://DC2.example.com -b 'CN=gmsaeks,CN=Managed Service Accounts,DC=example,DC=com' -s sub "(objectClass=msDs-GroupManagedServiceAccount)" msDS-ManagedPassword +SASL/GSSAPI authentication started +SASL username: eks-portable-ident@EXAMPLE.COM +SASL SSF: 256 +SASL data security layer installed. +dotnet /usr/sbin/credentials_fetcher_utf16_private.exe | kinit -c /var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc -V 'gmsaeks$'@EXAMPLE.COM +Using specified cache: /var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc +Using principal: gmsaeks$@EXAMPLE.COM +Password for gmsaeks$@EXAMPLE.COM: +Authenticated to Kerberos v5 +kinit return value = 0 +gMSA ticket is at /var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc +Thread 0: top of stack near 0x7fc7f18c7c88; argv_string=krb_ticket_refresh_thread + +``` + +#build the RPM for distribution + +``` +sudo yum install -y rpmdevtools rpmlint -y +sudo yum install chrpath + +rpmdev-setuptree + +sudo ln -s /usr/local/bin/cmake /usr/bin/cmake3 + + +cp ~/credentials-fetcher/package/credentials-fetcher.spec ~/rpmbuild/SPECS +mkdir ~/tmp + +cp -r credentials-fetcher/ ~/tmp + +cd ~/tmp && mv credentials-fetcher credentials-fetcher-v.1.2.0 +tar cvfz v.1.2.0.tar.gz credentials-fetcher-v.1.2.0 +cp v.1.2.0.tar.gz ~/rpmbuild/SOURCES/ + +rpmbuild -ba ~/rpmbuild/SPECS/credentials-fetcher.spec + +``` diff --git a/auth/kerberos/src/krb.cpp b/auth/kerberos/src/krb.cpp index 2100db4a..7dc09862 100644 --- a/auth/kerberos/src/krb.cpp +++ b/auth/kerberos/src/krb.cpp @@ -1,11 +1,11 @@ #include "daemon.h" -#include -#include #include +#include +#include #include +#include #include #include -#include // renew the ticket 1 hrs before the expiration #define RENEW_TICKET_HOURS 1 @@ -18,7 +18,7 @@ static const std::string install_path_for_decode_exe = "/usr/sbin/credentials_fetcher_utf16_private.exe"; static const std::string install_path_for_aws_cli = "/usr/bin/aws"; -extern "C" int my_kinit_main(int, char **); +extern "C" int my_kinit_main( int, char** ); /** * Check if binary is writable other than root @@ -77,7 +77,8 @@ static std::pair exec_shell_cmd( std::string cmd ) * @return result pair (error-code - 0 if successful * string of the form EC2AMAZ-Q5VJZQ$@CONTOSO.COM') */ -static std::pair get_machine_principal( std::string domain_name, creds_fetcher::CF_logger& cf_logger ) +static std::pair get_machine_principal( std::string domain_name, + creds_fetcher::CF_logger& cf_logger ) { std::pair result; @@ -115,11 +116,14 @@ static std::pair get_machine_principal( std::string domain_nam std::string host_name = hostname_result.second; // truncate the hostname to the host name size limit defined by microsoft - if(host_name.length() > HOST_NAME_LENGTH_LIMIT){ - cf_logger.logger( LOG_ERR, "WARNING: %s:%d hostname exceeds 15 characters," - "this can cause problems in getting kerberos tickets, please reduce hostname length", - __func__, __LINE__ ); - host_name = host_name.substr(0,HOST_NAME_LENGTH_LIMIT); + if ( host_name.length() > HOST_NAME_LENGTH_LIMIT ) + { + cf_logger.logger( + LOG_ERR, + "WARNING: %s:%d hostname exceeds 15 characters," + "this can cause problems in getting kerberos tickets, please reduce hostname length", + __func__, __LINE__ ); + host_name = host_name.substr( 0, HOST_NAME_LENGTH_LIMIT ); } /** @@ -177,11 +181,12 @@ int get_machine_krb_ticket( std::string domain_name, creds_fetcher::CF_logger& c result = get_machine_principal( std::move( domain_name ), cf_logger ); if ( result.first != 0 ) { - std::cout << "ERROR: " << __func__ << ":" << __LINE__ << " invalid machine principal" << std::endl; + std::cout << "ERROR: " << __func__ << ":" << __LINE__ << " invalid machine principal" + << std::endl; cf_logger.logger( LOG_ERR, "ERROR: %s:%d invalid machine principal", __func__, __LINE__ ); return result.first; } - + // kinit -kt /etc/krb5.keytab 'EC2AMAZ-GG97ZL$'@CONTOSO.COM std::transform( result.second.begin(), result.second.end(), result.second.begin(), []( unsigned char c ) { return std::toupper( c ); } ); @@ -242,17 +247,19 @@ int get_user_krb_ticket( std::string domain_name, std::string aws_sm_secret_name return -1; } - std::string command = - install_path_for_aws_cli + std::string( " secretsmanager get-secret-value --secret-id " ) + aws_sm_secret_name + " --query 'SecretString' --output text"; - // /usr/bin/aws secretsmanager get-secret-value --secret-id aws/directoryservices/d-xxxxxxxxxx/gmsa --query 'SecretString' --output text + std::string command = install_path_for_aws_cli + + std::string( " secretsmanager get-secret-value --secret-id " ) + + aws_sm_secret_name + " --query 'SecretString' --output text"; + // /usr/bin/aws secretsmanager get-secret-value --secret-id + // aws/directoryservices/d-xxxxxxxxxx/gmsa --query 'SecretString' --output text result = exec_shell_cmd( command ); // deserialize json to krb_ticket_info object Json::Value root; Json::CharReaderBuilder reader; - std::istringstream string_stream(result.second); + std::istringstream string_stream( result.second ); std::string errors; - Json::parseFromStream(reader, string_stream, &root, &errors); + Json::parseFromStream( reader, string_stream, &root, &errors ); // {"username":"user","password":"passw0rd"} std::string username = root["username"].asString(); std::string password = root["password"].asString(); @@ -261,13 +268,13 @@ int get_user_krb_ticket( std::string domain_name, std::string aws_sm_secret_name []( unsigned char c ) { return std::toupper( c ); } ); // kinit using api interface - char *kinit_argv[3]; + char* kinit_argv[3]; - kinit_argv[0] = (char *)"my_kinit"; + kinit_argv[0] = (char*)"my_kinit"; username = username + "@" + domain_name; - kinit_argv[1] = (char *)username.c_str(); - kinit_argv[2] = (char *)password.c_str(); - ret = my_kinit_main(2, kinit_argv); + kinit_argv[1] = (char*)username.c_str(); + kinit_argv[2] = (char*)password.c_str(); + ret = my_kinit_main( 2, kinit_argv ); #if 0 /* The old way */ std::string kinit_cmd = "echo '" + password + "' | kinit -V " + username + "@" + @@ -282,7 +289,6 @@ int get_user_krb_ticket( std::string domain_name, std::string aws_sm_secret_name return ret; } - /** * This function generates kerberos ticket with user with access to gMSA password credentials * User credentials must have adequate privileges to read gMSA passwords @@ -290,9 +296,8 @@ int get_user_krb_ticket( std::string domain_name, std::string aws_sm_secret_name * @param cf_daemon - parent daemon object * @return error-code - 0 if successful */ -int get_domainless_user_krb_ticket( std::string domain_name, std::string username, std::string - password, - creds_fetcher::CF_logger& cf_logger ) +int get_domainless_user_krb_ticket( std::string domain_name, std::string username, + std::string password, creds_fetcher::CF_logger& cf_logger ) { std::pair result; int ret; @@ -322,21 +327,20 @@ int get_domainless_user_krb_ticket( std::string domain_name, std::string usernam []( unsigned char c ) { return std::toupper( c ); } ); // kinit using api interface - char *kinit_argv[3]; + char* kinit_argv[3]; - kinit_argv[0] = (char *)"my_kinit"; + kinit_argv[0] = (char*)"my_kinit"; username = username + "@" + domain_name; - kinit_argv[1] = (char *)username.c_str(); - kinit_argv[2] = (char *)password.c_str(); - ret = my_kinit_main(2, kinit_argv); + kinit_argv[1] = (char*)username.c_str(); + kinit_argv[2] = (char*)password.c_str(); + ret = my_kinit_main( 2, kinit_argv ); username = "xxxx"; password = "xxxx"; - //TODO: nit - return pair later + // TODO: nit - return pair later return ret; } - /** * base64_decode - Decodes base64 encoded string * @param password - base64 encoded password @@ -381,7 +385,7 @@ static std::pair find_password( std::string ldap_search_result ) std::vector results; std::string password = std::string( "msDS-ManagedPassword::" ); - results = split_string(ldap_search_result, '#'); + results = split_string( ldap_search_result, '#' ); bool password_found = false; for ( auto& result : results ) { @@ -543,7 +547,7 @@ std::pair> get_domain_ips( std::string domain_name return std::make_pair( ips.first, list_of_ips ); } - list_of_ips = split_string(ips.second, '\n'); + list_of_ips = split_string( ips.second, '\n' ); return std::make_pair( EXIT_SUCCESS, list_of_ips ); } @@ -571,7 +575,7 @@ std::pair get_fqdn_from_domain_ip( std::string domain_ip, } std::vector list_of_dc_names; - list_of_dc_names = split_string(reverse_dns_output.second, '\n'); + list_of_dc_names = split_string( reverse_dns_output.second, '\n' ); for ( auto fqdn_str : list_of_dc_names ) { if ( fqdn_str.length() == 0 ) @@ -618,7 +622,7 @@ std::pair get_gmsa_krb_ticket( std::string domain_name, return std::make_pair( -1, std::string( "" ) ); } - results = split_string(domain_name, '.'); + results = split_string( domain_name, '.' ); std::string base_dn; /* Distinguished name */ for ( auto& result : results ) { @@ -626,11 +630,10 @@ std::pair get_gmsa_krb_ticket( std::string domain_name, } base_dn.pop_back(); // Remove last comma - std::string fqdn; - fqdn = retrieve_secret_from_ecs_config(domain_controller_gmsa); + fqdn = retrieve_secret_from_ecs_config( domain_controller_gmsa ); - if(fqdn.empty()) + if ( fqdn.empty() ) { std::pair> domain_ips = get_domain_ips( domain_name ); if ( domain_ips.first != 0 ) @@ -692,7 +695,7 @@ std::pair get_gmsa_krb_ticket( std::string domain_name, std::string default_principal = "'" + gmsa_account_name + "$'" + "@" + domain_name; /* Pipe password to the utf16 decoder and kinit */ - std::string kinit_cmd = std::string("dotnet ") + std::string( install_path_for_decode_exe ) + + std::string kinit_cmd = std::string( "dotnet " ) + std::string( install_path_for_decode_exe ) + std::string( " | kinit " ) + std::string( " -c " ) + krb_cc_name + " -V " + default_principal; std::cout << kinit_cmd << std::endl; @@ -718,22 +721,147 @@ std::pair get_gmsa_krb_ticket( std::string domain_name, } /** - * Parses the string that is a result of the klist command for the ticket experation date and time + * Parses the string that is a result of the klist command for the ticket expiration date and time * @param klist_ticket_info - String output of the klist command to parse - * @return - returns the date and time of the ticket experation otherwise an empty string + * @return - returns the date and time of the ticket expiration otherwise an empty string */ std::string get_ticket_expiration( std::string klist_ticket_info ) { - std::regex pattern(".+\n.{21}(.{19}).+"); + /* + * Ticket cache: KEYRING:persistent:1000:1000 + * Default principal: admin@CUSTOMERTEST.LOCAL + + * Valid starting Expires Service principal + * 12/04/2023 19:39:06 12/05/2023 05:39:06 krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL + * renew until 12/11/2023 19:39:04 + */ + + std::string any_regex( ".+" ); + std::string day_regex( "[0-9]{2}" ); + std::string month_regex( "[0-9]{2}" ); + std::string year_in_four_digits_regex( "[0-9]{4}" ); + std::string year_in_two_digits_regex( "[0-9]{2}" ); + std::string time_regex( "([0-9]{2}:[0-9]{2}:[0-9]{2})" ); + std::string separator_regex( "[/]{1}" ); + std::string space_regex( "[ ]+" ); + std::string left_paren_regex( "(" ); + std::string right_paren_regex( ")" ); + std::string krbtgt_regex( "krbtgt" ); + + std::string date_regex = left_paren_regex + day_regex + separator_regex + month_regex + + separator_regex + year_in_four_digits_regex + right_paren_regex; + + /* 12/04/2023 19:39:06 12/05/2023 05:39:06 krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL */ + std::string expires_regex = date_regex + space_regex + time_regex + space_regex + date_regex + + space_regex + time_regex + space_regex + krbtgt_regex; + + std::string regex_pattern( expires_regex ); + std::regex pattern( expires_regex ); std::smatch expires_match; - if (!std::regex_search(klist_ticket_info, expires_match, pattern)) + if ( !std::regex_search( klist_ticket_info, expires_match, pattern ) ) + { + // Retry with 2 digit year + /* 12/04/23 21:58:51 12/05/23 07:58:51 krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL */ + date_regex = left_paren_regex + day_regex + separator_regex + month_regex + + separator_regex + year_in_two_digits_regex + right_paren_regex; + expires_regex = date_regex + space_regex + time_regex + space_regex + date_regex + + space_regex + time_regex + space_regex + krbtgt_regex; + pattern = expires_regex; + if ( !std::regex_search( klist_ticket_info, expires_match, pattern ) ) + { + std::cout << "Unable to parse klist for ticket expiration: " << klist_ticket_info + << std::endl; + return std::string( "" ); + } + } + + /* + * From example above: + * 12/04/2023 + * 19:39:06 + * 12/05/2023 + * 05:39:06 + */ + std::string klist_valid_date; + std::string klist_valid_time; + std::string klist_expires_date; + std::string klist_expires_time; + for ( auto it = expires_match.cbegin(); it != expires_match.cend(); it++ ) + { + // First one is the full string + if ( it != expires_match.cbegin() ) + { + if ( klist_valid_date.empty() ) + { + klist_valid_date = *it; + continue; + } + if ( klist_valid_time.empty() ) + { + klist_valid_time = *it; + continue; + } + if ( klist_expires_date.empty() ) + { + klist_expires_date = *it; + continue; + } + if ( klist_expires_time.empty() ) + { + klist_expires_time = *it; + continue; + } + } + } + + return klist_expires_date; +} + +int test_get_ticket_expiration() +{ + std::string klist_string_with_4_digit_year( "Ticket cache: KEYRING:persistent:1000:1000\n\ + Default principal: admin@CUSTOMERTEST.LOCAL\n\ + \n\ + Valid starting Expires Service principal\n\ + 12/04/2023 19:39:06 12/05/2023 05:39:06 krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL\n\ + renew until 12/11/2023 19:39:04" ); + + std::string klist_string_with_2_digit_year( "Ticket cache: FILE:/tmp/krb5cc_2001112\n\ + Default principal: Admin@CUSTOMERTEST.LOCAL\n\ + \n\ + Valid starting Expires Service principal\n\ + 12/04/23 21:58:51 12/05/23 07:58:51 krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL\n\ + renew until 12/11/23 21:58:51\n\ + 12/04/23 21:58:51 12/05/23 07:58:51 EC2AMAZ-4MQOKF$@CUSTOMERTEST.LOCAL" ); + + std::string expire_date_4_digit_year( "12/05/2023" ); + std::string expire_date_2_digit_year( "12/05/23" ); + bool test_4_digit_year = false; + bool test_2_digit_year = false; + + test_4_digit_year = + ( get_ticket_expiration( klist_string_with_4_digit_year ) == expire_date_4_digit_year ); + + if ( test_4_digit_year ) + { + std::cout << "Self test for ticket expiration with 4 digit year is successful" << std::endl; + } + + test_2_digit_year = + ( get_ticket_expiration( klist_string_with_2_digit_year ) == expire_date_2_digit_year ); + if ( test_2_digit_year ) { - std::cout << "Unable to parse klist for ticket experation: " << klist_ticket_info << std::endl; - return std::string(""); + std::cout << "Self test for ticket expiration with 2 digit year is successful" << std::endl; } - return std::string(expires_match[1]); + if ( test_4_digit_year && test_2_digit_year ) + { + return EXIT_SUCCESS; + } + + std::cout << "**ERROR**: Failed self test for ticket expiration" << std::endl; + return EXIT_FAILURE; } /** @@ -755,10 +883,10 @@ bool is_ticket_ready_for_renewal( creds_fetcher::krb_ticket_info* krb_ticket_inf std::vector results; - results = split_string(krb_ticket_info_result.second, '#'); + results = split_string( krb_ticket_info_result.second, '#' ); std::string renew_until = "renew until"; bool is_ready_for_renewal = false; - + for ( auto& result : results ) { auto found = result.find( renew_until ); @@ -766,7 +894,17 @@ bool is_ticket_ready_for_renewal( creds_fetcher::krb_ticket_info* krb_ticket_inf { std::string renewal_date_time; - renewal_date_time = get_ticket_expiration(result); + /* + * Ticket cache: KEYRING:persistent:1000:1000 + * Default principal: admin@CUSTOMERTEST.LOCAL + + * Valid starting Expires Service principal + * 12/04/2023 19:39:06 12/05/2023 05:39:06 + krbtgt/CUSTOMERTEST.LOCAL@CUSTOMERTEST.LOCAL + * renew until 12/11/2023 19:39:04 + */ + + renewal_date_time = get_ticket_expiration( result ); char renewal_date[80]; char renewal_time[80]; @@ -814,16 +952,17 @@ bool is_ticket_ready_for_renewal( creds_fetcher::krb_ticket_info* krb_ticket_inf * @param username * @param password */ -std::list renew_kerberos_tickets_domainless(std::string krb_files_dir, std::string - domain_name, - std::string username, std::string password, - creds_fetcher::CF_logger& cf_logger ) +std::list renew_kerberos_tickets_domainless( std::string krb_files_dir, + std::string domain_name, + std::string username, + std::string password, + creds_fetcher::CF_logger& cf_logger ) { std::list renewed_krb_ticket_paths; // identify the metadata files in the krb directory std::vector metadatafiles; - for ( std::filesystem::recursive_directory_iterator end, dir( krb_files_dir ); - dir != end; ++dir ) + for ( std::filesystem::recursive_directory_iterator end, dir( krb_files_dir ); dir != end; + ++dir ) { auto path = dir->path(); if ( std::filesystem::is_regular_file( path ) ) @@ -849,12 +988,14 @@ std::list renew_kerberos_tickets_domainless(std::string krb_files_d for ( auto krb_ticket : krb_ticket_info_list ) { std::string domainlessuser = krb_ticket->domainless_user; - if(!username.empty() && username == domainlessuser) + if ( !username.empty() && username == domainlessuser ) { std::pair gmsa_ticket_result; std::string krb_cc_name = krb_ticket->krb_file_path; // gMSA kerberos ticket generation needs to have ldap over kerberos - // if the ticket exists for the machine/user already reuse it for getting gMSA password else retry the ticket creation again after generating user/machine kerberos ticket + // if the ticket exists for the machine/user already reuse it for getting gMSA + // password else retry the ticket creation again after generating user/machine + // kerberos ticket int num_retries = 2; for ( int i = 0; i < num_retries; i++ ) { @@ -865,16 +1006,18 @@ std::list renew_kerberos_tickets_domainless(std::string krb_files_d { if ( num_retries == 0 ) { - cf_logger.logger( LOG_WARNING, - "WARNING: Cannot get gMSA krb ticket " - "because of expired user/machine ticket, " - "will be retried automatically, service_account_name = %s", - krb_ticket->service_account_name.c_str() ); + cf_logger.logger( + LOG_WARNING, + "WARNING: Cannot get gMSA krb ticket " + "because of expired user/machine ticket, " + "will be retried automatically, service_account_name = %s", + krb_ticket->service_account_name.c_str() ); } else { - cf_logger.logger( LOG_ERR, "ERROR: Cannot get gMSA krb ticket using account %s", - krb_ticket->service_account_name.c_str() ); + cf_logger.logger( LOG_ERR, + "ERROR: Cannot get gMSA krb ticket using account %s", + krb_ticket->service_account_name.c_str() ); } // if tickets are created in domainless mode std::string domainless_user = krb_ticket->domainless_user; @@ -974,7 +1117,7 @@ std::vector delete_krb_tickets( std::string krb_files_dir, std::str return delete_krb_ticket_paths; } -std::string retrieve_secret_from_ecs_config(std::string ecs_variable_name) +std::string retrieve_secret_from_ecs_config( std::string ecs_variable_name ) { const char* ecs_config_file_name = "/etc/ecs/ecs.config"; // TBD:: Add commandline if needed @@ -985,7 +1128,7 @@ std::string retrieve_secret_from_ecs_config(std::string ecs_variable_name) while ( std::getline( config_file, line ) ) { // TBD: Error handling for incorrectly formatted /etc/ecs/ecs.config - results = split_string(line, '='); + results = split_string( line, '=' ); std::string key = results[0]; std::string value = results[1]; if ( ecs_variable_name.compare( key ) == 0 ) @@ -999,19 +1142,19 @@ std::string retrieve_secret_from_ecs_config(std::string ecs_variable_name) /** * Given an input string split based on provided delimiter and return the split strings as vector - * + * * @param input_string - input string to split * @param delimiter - char to split the input string on * @return results - results to store vector of strings after `input_string` is split -*/ -std::vector split_string(std::string input_string, char delimiter) + */ +std::vector split_string( std::string input_string, char delimiter ) { std::vector results; - std::istringstream input_string_stream(input_string); + std::istringstream input_string_stream( input_string ); std::string token; - while (std::getline(input_string_stream, token, delimiter)) + while ( std::getline( input_string_stream, token, delimiter ) ) { - results.push_back(token); + results.push_back( token ); } return results; } diff --git a/auth/kinit_client/kinit.c b/auth/kinit_client/kinit.c index 6478b185..6bda12bd 100644 --- a/auth/kinit_client/kinit.c +++ b/auth/kinit_client/kinit.c @@ -562,6 +562,7 @@ k5_kinit(struct k_opts *opts, struct k5_data *k5) fprintf(stderr, _("Initialized cache\n")); ret = k5_cc_store_primary_cred(k5->ctx, mcc, &my_creds); + if (ret) { com_err(progname, ret, _("while storing credentials")); goto cleanup; @@ -605,7 +606,30 @@ k5_kinit(struct k_opts *opts, struct k5_data *k5) krb5_kt_close(k5->ctx, keytab); return notix ? 0 : 1; } +#define IS_TGS_PRINC(p) ((p)->length == 2 && \ + data_eq_string((p)->data[0], KRB5_TGS_NAME)) +//k5_cc_store_primary_cred() is not in Krb5 package available for Amazon Linux 2 so lets pull the code in here + +krb5_error_code +k5_cc_store_primary_cred(krb5_context context, krb5_ccache cache, + krb5_creds *creds) +{ + krb5_error_code ret; + + /* Write a start realm if we're writing a TGT and the client realm isn't + * the same as the TGS realm. */ + if (IS_TGS_PRINC(creds->server) && + !data_eq(creds->client->realm, creds->server->data[1])) { + ret = krb5_cc_set_config(context, cache, NULL, + KRB5_CC_CONF_START_REALM, + &creds->server->data[1]); + if (ret) + return ret; + } + + return krb5_cc_store_cred(context, cache, creds); +} int my_kinit_main(int argc, char *argv[]) { diff --git a/common/daemon.h b/common/daemon.h index 3750ca61..09e2d22f 100644 --- a/common/daemon.h +++ b/common/daemon.h @@ -155,6 +155,7 @@ int read_meta_data_json_test(); int read_meta_data_invalid_json_test(); int write_meta_data_json_test(); int renewal_failure_krb_dir_not_found_test(); +int test_get_ticket_expiration(); /** * Methods in config module @@ -177,7 +178,9 @@ int parse_cred_spec( std::string credspec_data, creds_fetcher::krb_ticket_info* int parse_cred_file_path(const std::string& cred_file_path, std::string& cred_file, std::string& cred_file_lease_id ); -int ProcessCredSpecFile(std::string krb_files_dir, std::string credspec_filepath, creds_fetcher::CF_logger& cf_logger, std::string cred_file_lease_id); +int ProcessCredSpecFile(std::string krb_files_dir, std::string credspec_filepath, + creds_fetcher::CF_logger& cf_logger, std::string cred_file_lease_id, + std::string aws_sm_secret_name, bool is_diagnostic); std::string generate_lease_id(); @@ -191,6 +194,8 @@ int krb_ticket_renew_handler( creds_fetcher::Daemon cf_daemon ); */ bool contains_invalid_characters( const std::string& path ); std::list read_meta_data_json( std::string file_path ); +std::list read_meta_data_json( std::string file_path, + std::string alt_file_path, std::string krb_file_path ); int write_meta_data_json( creds_fetcher::krb_ticket_info* krb_ticket_info, std::string lease_id, std::string krb_files_dir ); diff --git a/config/src/config.cpp b/config/src/config.cpp index b312881a..ced58655 100644 --- a/config/src/config.cpp +++ b/config/src/config.cpp @@ -9,11 +9,12 @@ void print_help( const struct option* long_options, const std::map& options_descriptions ) { std::cout << "Usage: " << std::endl; - std::cout << "Runtime Environment Variables:" << std::endl; - std::cout << "CF_CRED_SPEC_FILE=:" << std::endl; - std::cout << "\t\tSet to a path of a json credential file." << std::endl; - std::cout << "\t\tUse an optional colon followed by a lease identifier (Default: " - << DEFAULT_CRED_FILE_LEASE_ID << ")" << std::endl; + std::cout << "Runtime Environment Variables:" << std::endl; + std::cout << "CF_CRED_SPEC_FILE=:" << std::endl; + std::cout << "\t\tSet to a path of a json credential file." << std::endl; + std::cout + << "\t\tUse an optional colon followed by a lease identifier (Default: " + << DEFAULT_CRED_FILE_LEASE_ID << ")" << std::endl; std::cout << "\nAllowed options" << std::endl; size_t max_option_length = 0; for ( const struct option* opt = long_options; opt->name != nullptr; ++opt ) @@ -78,7 +79,8 @@ int parse_options( int argc, const char* argv[], creds_fetcher::Daemon& cf_daemo "Manager (in same region)" }, { "version", "Version of credentials-fetcher" } }; int option; - while ( ( option = getopt_long( argc, (char* const*)argv, "htv:s:n", long_options, nullptr ) ) != -1 ) + while ( ( option = getopt_long( argc, (char* const*)argv, "htv:s:n", long_options, + nullptr ) ) != -1 ) { switch ( option ) { @@ -93,9 +95,10 @@ int parse_options( int argc, const char* argv[], creds_fetcher::Daemon& cf_daemo std::cout << "Verbosity level was set to " << optarg << std::endl; break; case 's': - cf_daemon.aws_sm_secret_name = optarg; + cf_daemon.aws_sm_secret_name = std::string( optarg ); std::cout << "Option selected for domainless operation, AWS secrets manager " - "secret-name = " << optarg << std::endl; + "secret-name = " + << optarg << std::endl; break; case 'n': std::cout << CMAKE_PROJECT_VERSION << std::endl; @@ -105,14 +108,16 @@ int parse_options( int argc, const char* argv[], creds_fetcher::Daemon& cf_daemo return EXIT_FAILURE; } } - std::string aws_sm_secret_name = retrieve_secret_from_ecs_config(domainless_gmsa_field); - cf_daemon.aws_sm_secret_name = aws_sm_secret_name; + if ( cf_daemon.aws_sm_secret_name.empty() ) + { + cf_daemon.aws_sm_secret_name = retrieve_secret_from_ecs_config( domainless_gmsa_field ); + } } catch ( const std::exception& ex ) { std::cout << "Run with --help to see options" << std::endl; return EXIT_FAILURE; } - + return EXIT_SUCCESS; } diff --git a/daemon/src/daemon.cpp b/daemon/src/daemon.cpp index fde229f2..174416fc 100644 --- a/daemon/src/daemon.cpp +++ b/daemon/src/daemon.cpp @@ -13,8 +13,6 @@ struct thread_info char* argv_string; /* From command-line argument */ }; -static const char* grpc_thread_name = "grpc_thread"; - static void systemd_shutdown_signal_catcher( int signo ) { cf_daemon.got_systemd_shutdown_signal = 1; @@ -35,24 +33,6 @@ static void systemd_shutdown_signal_catcher( int signo ) #define ENV_CF_CRED_SPEC_FILE "CF_CRED_SPEC_FILE" -/** - * grpc_thread_start - used in pthread_create - * @param arg - thread info - * @return pthread name - */ -void* grpc_thread_start( void* arg ) -{ - struct thread_info* tinfo = (struct thread_info*)arg; - - printf( "Thread %d: top of stack near %p; argv_string=%s\n", tinfo->thread_num, (void*)&tinfo, - tinfo->argv_string ); - - RunGrpcServer( cf_daemon.unix_socket_dir, cf_daemon.krb_files_dir, cf_daemon.cf_logger, - &cf_daemon.got_systemd_shutdown_signal, cf_daemon.aws_sm_secret_name ); - - return tinfo->argv_string; -} - /** * refresh_krb_tickets - used in pthread_create * @param arg - thread info @@ -135,18 +115,20 @@ std::pair create_pthread( void* ( *func )(void*), const char* pthrea return std::make_pair( EXIT_SUCCESS, tinfo ); } -int parse_cred_file_path(const std::string& cred_file_path, std::string& cred_file, std::string& cred_file_lease_id ) +int parse_cred_file_path( const std::string& cred_file_path, std::string& cred_file, + std::string& cred_file_lease_id ) { size_t colon_delim_pos; char path_lease_delimiter = ':'; - if (cred_file_path.empty()) + if ( cred_file_path.empty() ) return EXIT_FAILURE; - colon_delim_pos = cred_file_path.find(path_lease_delimiter); + colon_delim_pos = cred_file_path.find( path_lease_delimiter ); - //If the : delimiter is not found, then assume the cred_file_path is just the path to cred spec file and use the default lease id - if (colon_delim_pos == std::string::npos) + // If the : delimiter is not found, then assume the cred_file_path is just the path to cred spec + // file and use the default lease id + if ( colon_delim_pos == std::string::npos ) { cred_file = cred_file_path; cred_file_lease_id = DEFAULT_CRED_FILE_LEASE_ID; @@ -154,8 +136,8 @@ int parse_cred_file_path(const std::string& cred_file_path, std::string& cred_fi return EXIT_SUCCESS; } - cred_file = cred_file_path.substr(0, colon_delim_pos); - cred_file_lease_id = cred_file_path.substr(colon_delim_pos+1); + cred_file = cred_file_path.substr( 0, colon_delim_pos ); + cred_file_lease_id = cred_file_path.substr( colon_delim_pos + 1 ); return EXIT_SUCCESS; } @@ -164,8 +146,7 @@ int main( int argc, const char* argv[] ) { std::pair pthread_status; std::string cred_file; - std::string cred_file_lease_id; - void* grpc_pthread; + std::string cred_file_lease_id( "fixed_lease_id" ); void* krb_refresh_pthread; int status = parse_options( argc, argv, cf_daemon ); @@ -178,21 +159,80 @@ int main( int argc, const char* argv[] ) cf_daemon.logging_dir = CF_LOGGING_DIR; cf_daemon.unix_socket_dir = CF_UNIX_DOMAIN_SOCKET_DIR; - if ( getenv(ENV_CF_CRED_SPEC_FILE) != NULL) + /** + * Domain name and gmsa account are usually set in APIs. + * The options below can be used as a test. + */ + cf_daemon.domain_name = CF_TEST_DOMAIN_NAME; + cf_daemon.gmsa_account_name = CF_TEST_GMSA_ACCOUNT; + + std::cout << "krb_files_dir = " << cf_daemon.krb_files_dir << std::endl; + std::cout << "cred_file = " << cf_daemon.cred_file << " (lease id: " << cred_file_lease_id + << ")" << std::endl; + std::cout << "logging_dir = " << cf_daemon.logging_dir << std::endl; + std::cout << "unix_socket_dir = " << cf_daemon.unix_socket_dir << std::endl; + + if ( cf_daemon.run_diagnostic ) + { + int found_cred_spec_result = EXIT_SUCCESS; + int not_found_cred_spec_result = EXIT_SUCCESS; + int is_good_cred_spec = 0; + if ( getenv( ENV_CF_CRED_SPEC_FILE ) != NULL ) + { + std::string sample_cred_spec_file( getenv( ENV_CF_CRED_SPEC_FILE ) ); + found_cred_spec_result = parse_cred_file_path( sample_cred_spec_file.c_str(), cred_file, + cred_file_lease_id ); + not_found_cred_spec_result = + parse_cred_file_path( "", cred_file, cred_file_lease_id ); + is_good_cred_spec = ProcessCredSpecFile( + cf_daemon.krb_files_dir, sample_cred_spec_file.c_str(), cf_daemon.cf_logger, + cred_file_lease_id, cf_daemon.aws_sm_secret_name, true ); + if ( ( found_cred_spec_result == EXIT_SUCCESS ) && + ( not_found_cred_spec_result == EXIT_FAILURE ) && + ( is_good_cred_spec == EXIT_SUCCESS ) ) + { + std::cout << "Cred spec tests successful" << std::endl; + } + else + { + std::cout << "***ERROR**: Cred spec tests failed" << std::endl; + } + } + bool is_ticket_expiration_test_sucessful = (test_get_ticket_expiration() == EXIT_SUCCESS); + bool exit_status = ( read_meta_data_json_test() == EXIT_SUCCESS ) && + ( read_meta_data_invalid_json_test() == EXIT_SUCCESS ) && + ( renewal_failure_krb_dir_not_found_test() == EXIT_SUCCESS ) && + ( write_meta_data_json_test() == EXIT_SUCCESS ) && + ( found_cred_spec_result == EXIT_SUCCESS ) && + ( not_found_cred_spec_result == EXIT_FAILURE ) && + ( is_good_cred_spec == EXIT_SUCCESS ) && + is_ticket_expiration_test_sucessful; + if ( exit_status ) + { + exit( 0 ); + } + else + { + exit( 1 ); + } + } + + if ( getenv( ENV_CF_CRED_SPEC_FILE ) != NULL ) { - int parseResult = parse_cred_file_path( getenv(ENV_CF_CRED_SPEC_FILE), - cred_file, - cred_file_lease_id); + int parseResult = + parse_cred_file_path( getenv( ENV_CF_CRED_SPEC_FILE ), cred_file, cred_file_lease_id ); - if (parseResult == EXIT_FAILURE) + if ( parseResult == EXIT_FAILURE ) { - std::cout << "Failed parsing environment variable " << getenv(ENV_CF_CRED_SPEC_FILE) << std::endl; - cf_daemon.cf_logger.logger( LOG_ERR, "Failed parsing environment variable %s", getenv(ENV_CF_CRED_SPEC_FILE) ); + std::cout << "Failed parsing environment variable " << getenv( ENV_CF_CRED_SPEC_FILE ) + << std::endl; + cf_daemon.cf_logger.logger( LOG_ERR, "Failed parsing environment variable %s", + getenv( ENV_CF_CRED_SPEC_FILE ) ); - exit( EXIT_FAILURE); + exit( EXIT_FAILURE ); } - if (!std::filesystem::exists( cred_file)) + if ( !std::filesystem::exists( cred_file ) ) { cred_file_lease_id.clear(); std::cout << "Ignoring CF_CREF_FILE, file " << cred_file << " not found" << std::endl; @@ -203,25 +243,6 @@ int main( int argc, const char* argv[] ) } } - /** - * Domain name and gmsa account are usually set in APIs. - * The options below can be used as a test. - */ - cf_daemon.domain_name = CF_TEST_DOMAIN_NAME; - cf_daemon.gmsa_account_name = CF_TEST_GMSA_ACCOUNT; - - std::cout << "krb_files_dir = " << cf_daemon.krb_files_dir << std::endl; - std::cout << "cred_file = " << cf_daemon.cred_file << " (lease id: " << cred_file_lease_id << ")" << std::endl; - std::cout << "logging_dir = " << cf_daemon.logging_dir << std::endl; - std::cout << "unix_socket_dir = " << cf_daemon.unix_socket_dir << std::endl; - - if ( cf_daemon.run_diagnostic ) - { - exit( read_meta_data_json_test() || - read_meta_data_invalid_json_test() || renewal_failure_krb_dir_not_found_test() || - write_meta_data_json_test() ); - } - struct sigaction sa; cf_daemon.got_systemd_shutdown_signal = 0; memset( &sa, 0, sizeof( struct sigaction ) ); @@ -234,31 +255,23 @@ int main( int argc, const char* argv[] ) /* We need to run three parallel processes */ // 1. Systemd - daemon - // 2. grpc server - // 3. timer to run every 45 min - - if ( !cf_daemon.cred_file.empty() ) { - cf_daemon.cf_logger.logger( LOG_INFO, "Credential file exists %s", cf_daemon.cred_file.c_str() ); - - int specFileReturn = ProcessCredSpecFile(cf_daemon.krb_files_dir, cf_daemon.cred_file, cf_daemon.cf_logger, cred_file_lease_id); - if (specFileReturn == EXIT_FAILURE) { + // 2. timer to run every 45 min + + if ( !cf_daemon.cred_file.empty() ) + { + cf_daemon.cf_logger.logger( LOG_INFO, "Credential file exists %s", + cf_daemon.cred_file.c_str() ); + + int specFileReturn = + ProcessCredSpecFile( cf_daemon.krb_files_dir, cf_daemon.cred_file, cf_daemon.cf_logger, + cred_file_lease_id, cf_daemon.aws_sm_secret_name, false ); + if ( specFileReturn == EXIT_FAILURE ) + { std::cout << "ProcessCredSpecFile() non 0 " << std::endl; exit( EXIT_FAILURE ); } } - - /* Create one pthread for gRPC processing */ - pthread_status = - create_pthread( grpc_thread_start, grpc_thread_name, -1 ); - if ( pthread_status.first < 0 ) - { - cf_daemon.cf_logger.logger( LOG_ERR, "Error %d: Cannot create pthreads", - pthread_status.first ); - exit( EXIT_FAILURE ); - } - grpc_pthread = pthread_status.second; - cf_daemon.cf_logger.logger( LOG_INFO, "grpc pthread is at %p", grpc_pthread ); - + /* Create pthread for refreshing krb tickets */ pthread_status = create_pthread( refresh_krb_tickets_thread_start, "krb_ticket_refresh_thread", -1 ); @@ -320,4 +333,3 @@ int main( int argc, const char* argv[] ) return EXIT_SUCCESS; } - diff --git a/daemon/src/gmsa_minimal_service.cpp b/daemon/src/gmsa_minimal_service.cpp new file mode 100644 index 00000000..2b8162cc --- /dev/null +++ b/daemon/src/gmsa_minimal_service.cpp @@ -0,0 +1,216 @@ +//Extraction of methods that are needed for command line moe (No gRPC) that are intertwined with gRpc +#include "daemon.h" + +#include +#include +#include +#include + +#define LEASE_ID_LENGTH 10 +#define UNIX_SOCKET_NAME "credentials_fetcher.sock" +#define INPUT_CREDENTIALS_LENGTH 256 + +static const std::vector invalid_characters = { + '&', '|', ';', '$', '*', '?', '<', '>', '!',' '}; + +/** + * This function parses the cred spec file. + * The cred spec file is in json format. + * @param credspec - service account information + * @param krb_ticket_info - return service account info + * @return + */ +int parse_cred_spec( std::string credspec_data, creds_fetcher::krb_ticket_info* krb_ticket_info ) +{ + try + { + if ( credspec_data.empty() ) + { + fprintf( stderr, SD_CRIT "credspec is empty" ); + return -1; + } + + Json::Value root; + Json::CharReaderBuilder reader; + std::istringstream credspec_stream(credspec_data); + std::string errors; + Json::parseFromStream(reader, credspec_stream, &root, &errors); + // get domain name from credspec + std::string domain_name = root["DomainJoinConfig"]["DnsName"].asString(); + // get service account name from credspec + std::string service_account_name; + const Json::Value& gmsa_array = root["ActiveDirectoryConfig"]["GroupManagedServiceAccounts"]; + for (const Json::Value& gmsa : gmsa_array) + { + service_account_name = gmsa["Name"].asString(); + if (!service_account_name.empty()) + break; + } + if (service_account_name.empty() || domain_name.empty()) + return -1; + + krb_ticket_info->domain_name = domain_name; + krb_ticket_info->service_account_name = service_account_name; + } + catch ( ... ) + { + fprintf( stderr, SD_CRIT "credspec is not properly formatted" ); + return -1; + } + + return 0; +} + + +/** + * ProcessCredSpecFile - Processes a provided credential spec file + * @param krb_files_dir - Kerberos TGT directory + * @param credspec_filepath - Path to credential spec file produced by DC + * @param cf_logger - log to systemd daemon + * @param cred_file_lease_id - The lease id to use for this credential spec file + * @return - return 0 on success + */ +int ProcessCredSpecFile(std::string krb_files_dir, std::string credspec_filepath, creds_fetcher::CF_logger& cf_logger, + std::string cred_file_lease_id, std::string aws_sm_secret_name, bool self_test) { + std::unordered_set krb_ticket_dirs; + std::string err_msg; + std::string credspec_contents; + int status; + + cf_logger.logger( LOG_INFO, "Generating lease id %s", cred_file_lease_id ); + + if ( !std::filesystem::exists( credspec_filepath ) ){ + std::cerr << "The credential spec file " << credspec_filepath << " was not found!" << std::endl; + cf_logger.logger( LOG_ERR, "The credential spec file %s was not found!", + credspec_filepath.c_str() ); + return EXIT_FAILURE; + } + + std::ifstream inputFile(credspec_filepath); + if (inputFile.is_open()) + { + credspec_contents.assign((std::istreambuf_iterator(inputFile)), + std::istreambuf_iterator()); + + inputFile.close(); // Close the file + } + else + { + cf_logger.logger( LOG_ERR, "Unable to open credential spec file: %s", credspec_filepath); + std::cerr << "Unable to open credential spec file: " << credspec_filepath << std::endl; + + return EXIT_FAILURE; + } + + creds_fetcher::krb_ticket_info* krb_ticket_info = new creds_fetcher::krb_ticket_info; + int parse_result = parse_cred_spec( credspec_contents, krb_ticket_info ); + + // only add the ticket info if the parsing is successful + if ( parse_result == EXIT_SUCCESS ) + { + std::string krb_files_path = krb_files_dir + "/" + cred_file_lease_id + "/" + + krb_ticket_info->service_account_name; + krb_ticket_info->krb_file_path = krb_files_path; + krb_ticket_info->domainless_user = ""; + if (self_test) return EXIT_SUCCESS; + } + else + { + err_msg = "Error: credential spec provided is not properly formatted"; + if (self_test) return EXIT_FAILURE; + } + + if ( err_msg.empty() ) + { + if ( aws_sm_secret_name.length() != 0 ) + { + status = get_user_krb_ticket( krb_ticket_info->domain_name, + aws_sm_secret_name, cf_logger ); + krb_ticket_info->domainless_user = + "awsdomainlessusersecret:"+aws_sm_secret_name; + + if ( status < 0 ) + { + cf_logger.logger( LOG_ERR, "Error %d: Cannot get usr krb ticket", + status ); + delete krb_ticket_info; + return EXIT_FAILURE; + } + } + else + { + // invoke to get machine ticket + status = get_machine_krb_ticket( krb_ticket_info->domain_name, cf_logger ); + if ( status < 0 ) + { + cf_logger.logger( LOG_ERR, "Error %d: Cannot get machine krb ticket", + status ); + delete krb_ticket_info; + return EXIT_FAILURE; + } + } + + std::string krb_file_path = krb_ticket_info->krb_file_path; + if ( std::filesystem::exists( krb_file_path ) ) + { + cf_logger.logger( LOG_INFO, + "Deleting existing credential file directory %s", ++ krb_file_path.c_str() ); + + std::filesystem::remove_all(krb_file_path); + } + std::filesystem::create_directories( krb_file_path ); + + std::string krb_ccname_str = krb_ticket_info->krb_file_path + "/krb5cc"; + + if ( !std::filesystem::exists( krb_ccname_str ) ) + { + std::ofstream file( krb_ccname_str ); + file.close(); + + krb_ticket_info->krb_file_path = krb_ccname_str; + } + + std::pair gmsa_ticket_result = get_gmsa_krb_ticket( + krb_ticket_info->domain_name, krb_ticket_info->service_account_name, + krb_ccname_str, cf_logger ); + if ( gmsa_ticket_result.first != 0 ) + { + err_msg = "ERROR: Cannot get gMSA krb ticket"; + std::cout << err_msg << std::endl; + cf_logger.logger( LOG_ERR, "ERROR: Cannot get gMSA krb ticket", + status ); + } + else + { + chmod(krb_ccname_str.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + + cf_logger.logger( LOG_INFO, "gMSA ticket is at %s", + gmsa_ticket_result.second.c_str() ); + std::cout << "gMSA ticket is at " << gmsa_ticket_result.second + << std::endl; + } + } + + // And we are done! Let the gRPC runtime know we've finished, using the + // memory address of this instance as the uniquely identifying tag for + // the event. + if ( !err_msg.empty() ) + { + // remove the directory on failure + std::filesystem::remove_all( krb_ticket_info->krb_file_path ); + + std::cerr << err_msg << std::endl; + cf_logger.logger( LOG_ERR, "%s", err_msg.c_str() ); + delete krb_ticket_info; + + return EXIT_FAILURE; + } + + // write the ticket information to meta data file + write_meta_data_json( krb_ticket_info, cred_file_lease_id, krb_files_dir ); + + delete krb_ticket_info; + + return EXIT_SUCCESS; +} diff --git a/doc/eks_smb_use_case.md b/doc/eks_smb_use_case.md new file mode 100644 index 00000000..05737d5a --- /dev/null +++ b/doc/eks_smb_use_case.md @@ -0,0 +1,243 @@ + +# Using Credentials-Fetcher with EKS/SMB + + +### Example use case + +You run EKS pods on EC2 using Amazon Linux 2 and require access to SMB file shares using Kerberos authentication, even when the EKS worker nodes are not part of the Windows Active Directory domain. The use of Kerberos authentication is crucial to guarantee SMB encryption-in-transit for compliance purposes. + +### Solution + +Implement the SMB CSI Driver with Kerberos authentication settings to enable the mounting of SMB shares onto the pods. Employ the Credentials-Fetcher to generate and update Kerberos tickets as needed. + +![Alt text](eks_smb_use_case_diagram.png) + +### References + +* https://github.com/kubernetes-csi/csi-driver-smb +* https://aws.amazon.com/blogs/containers/using-amazon-fsx-for-windows-file-server-as-persistent-storage-on-amazon-eks/ +* https://aws.amazon.com/blogs/opensource/aws-now-supports-credentials-fetcher-for-gmsa-on-amazon-linux-2023/ +* + +### Prerequisites: + +* Microsoft Active Directory +* gMSA Account in Active Directory +* credspec.json generated from the gMSA Account +* User account with permissions to query gMSA account +* AWS Secrets Manager secret with username/password/domainName to the User Account +* FSx NetApp with an SMB File share on a Volume configured with NTFS permissions style +* FSx Netapp joined to Active Directory +* SMB File share permissions assigned to the gMSA account granting read/write +* Amazon EKS Cluster +* Amazon EKS Worker Node operating on Amazon Linux 2 +* EKS Worknode EC2 Instance Role configured with policy to read Secrets Manager secret +* kubectl installed and configured for EKS Cluster + + +### Install SMB CSI Driver + +``` +curl -skSL https://raw.githubusercontent.com/kubernetes-csi/csi-driver-smb/v1.13.0/deploy/install-driver.sh | bash -s v1.13.0 -- + +#smb cis driver will write empty kerberos ticket (stored as base64 encoded string) here. This ticket will not actually be used +mkdir -p /var/lib/kubelet/kerberos/ + +#needed to mount cifs(smb) +yum install -y cifs-utils -y +``` + +``` + +mkdir -p /etc/krb5.conf.d/ +echo "[libdefaults] +#SMB CSI Driver should look for krbcc files output by Credentials-Fetcher, +#not the placeholder ticket stored in Kubernetes secret +default_ccache_name = FILE:/var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc" > /etc/krb5.conf.d/ccache.conf + +``` + + + +### Create SMBCreds Secret + +``` +#This kerberos ticket cache won't actually be used +#just use an "empty" file +#cifs/smb mount will actually find anduse gMSA ticket in +#/var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc +#created by credentials-fetcher service + +echo "empty" > /tmp/emptycache +export EMPTYCACHEFILE=/tmp/emptycache +EMPTYCACHE=$(base64 -w 0 $EMPTYCACHEFILE) + +kubectl create secret generic smbcreds --from-literal krb5cc_0=$EMPTYCACHE + + +``` + +### Apply Persistent Volume (PV) + +create pv-smb.yaml with below contents: +``` +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + annotations: + pv.kubernetes.io/provisioned-by: smb.csi.k8s.io + name: pv-smb +spec: + capacity: + storage: 100Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: smb + mountOptions: + - sec=krb5 + - cruid=0 + - dir_mode=0777 + - file_mode=0777 + - uid=1000 + - gid=1000 + - noperm + - mfsymlinks + - cache=strict + - noserverino # required to prevent data corruption + csi: + driver: smb.csi.k8s.io + readOnly: false + # volumeHandle format: {smb-server-address}#{sub-dir-name}#{share-name} + # make sure this value is unique for every share in the cluster + volumeHandle: smb-server.default.svc.cluster.local/share## + volumeAttributes: + source: "//svm1.example.com/smb1" + nodeStageSecretRef: + name: smbcreds + namespace: default +``` + +``` +kubectl apply -f pv-smb.yaml +``` + + + +### Apply Persistent Volume Claim (PVC) + +Create pvc-smb.yaml with contents below: +``` +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: pvc-smb +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi + volumeName: pv-smb + storageClassName: smb +``` + +``` +kubectl apply -f pvc-smb.yaml +``` + +![Alt text](eks_smb_use_case_pv.png) + + +### Install Credentials-Fetcher Amazon Linux 2 RPM + +``` +#add Microsoft repo so .Net 6 dependency can be installed +sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm + +yum install credentials-fetcher-1.2.0-3.amzn2.x86_64.rpm -y + +``` +### store credspec.json in /var/credentials-fetcher/credspec.json + +### configure AWS region to your region +``` +aws configure set default.region us-east-1 +``` +### Set environment variables and command line params for [credentials-fetcher.service](https://github.com/awsjohns/credentials-fetcher/blob/34777570e5c49d93258038d6d4c6d8bef33d306b/scripts/systemd/credentials-fetcher.service) + +* Edit /usr/lib/systemd/system/credentials-fetcher.service + +``` +#we need to set so credentials-fetcher doesn't use whats in /etc/krb5.conf.d for the user ticket. +#/var/credentials-fetcher/krbdir/credspec/gmsaeks/krb5cc is for the gMSA ticket +Environment="KRB5CCNAME=/var/credentials-fetcher/krbdir/krb5cc" +#add secret parameter +ExecStart=/usr/sbin/credentials-fetcherd --aws_sm_secret_name gmsa-plugin-input +``` + +### Start daemon service + +``` +#start the service +sudo systemctl start credentials-fetcher.service + +#view the logs +sudo journalctl -u credentials-fetcher +``` + +![Alt text](eks_smb_use_case_start_service.png) + + +### Test with EKS Pod that reads/writes to SMB file share + + +create pv-smb-pod.yaml with below YAML: + +``` +apiVersion: v1 +kind: Pod +metadata: + name: pod-smb-reader-writer +spec: + volumes: + - name: smb-pv-storage + persistentVolumeClaim: + claimName: pvc-smb + containers: + - image: busybox + name: smb-writer + command: ["/bin/sh","-c","while true; do echo $(date) Logging data >> /output/output.log; echo wrote; sleep 5; done"] + volumeMounts: + - name: smb-pv-storage + mountPath: /output + - image: busybox + name: smb-reader + command: ['/bin/sh', '-c', 'tail -f /input/output.log'] + volumeMounts: + - name: smb-pv-storage + mountPath: /input +``` + +### Deploy the EKS pod + + + +``` +kubectl apply -f pv-smb-pod.yaml +``` + +### Confirm the pod has a Running status +``` + +# kubectl get pods +NAME READY STATUS RESTARTS AGE +pod-smb-reader-writer 2/2 Running 0 22s +``` + + +### Open the file on file share to confirm +![View the output](eks_smb_use_case_output.png +) \ No newline at end of file diff --git a/doc/eks_smb_use_case_diagram.png b/doc/eks_smb_use_case_diagram.png new file mode 100644 index 00000000..4de7615a Binary files /dev/null and b/doc/eks_smb_use_case_diagram.png differ diff --git a/doc/eks_smb_use_case_output.png b/doc/eks_smb_use_case_output.png new file mode 100644 index 00000000..07781045 Binary files /dev/null and b/doc/eks_smb_use_case_output.png differ diff --git a/doc/eks_smb_use_case_pv.png b/doc/eks_smb_use_case_pv.png new file mode 100644 index 00000000..0516acd3 Binary files /dev/null and b/doc/eks_smb_use_case_pv.png differ diff --git a/doc/eks_smb_use_case_start_service.png b/doc/eks_smb_use_case_start_service.png new file mode 100644 index 00000000..cfb4c01c Binary files /dev/null and b/doc/eks_smb_use_case_start_service.png differ diff --git a/metadata/CMakeLists.txt b/metadata/CMakeLists.txt index 920a3375..531ea309 100644 --- a/metadata/CMakeLists.txt +++ b/metadata/CMakeLists.txt @@ -5,7 +5,6 @@ project(metadata) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") FILE(GLOB SRC_FILES src/*.cpp tests/*.cpp) - set(metadata "${SRC_FILES}" PARENT_SCOPE) # test sample for unit tests @@ -20,4 +19,5 @@ file( FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) + enable_testing () diff --git a/metadata/src/metadata.cpp b/metadata/src/metadata.cpp index 66620d88..1813eceb 100644 --- a/metadata/src/metadata.cpp +++ b/metadata/src/metadata.cpp @@ -25,6 +25,83 @@ bool contains_invalid_characters( const std::string& path ) } return result; } + +/** + * read the kerberos ticket information from the cache + * @param file_path - file path for the metadata associated to a lease + * @param alt_file_path - alt file path for the metadata associated to a lease + * @param krb_files_dir - path of the dir for kerberos tickets + * @return vector of kerberos ticket info + */ +std::list read_meta_data_json( std::string file_path, + std::string alt_file_path, std::string krb_files_dir ) +{ + std::list krb_ticket_info_list; + std::string metadata_file = file_path; + + if ( !metadata_file.empty() && !std::filesystem::exists( metadata_file ) ) + { + metadata_file = alt_file_path; + } + + try + { + if ( metadata_file.empty() ) + { + fprintf( stderr, SD_CRIT "meta data file is empty" ); + return krb_ticket_info_list; + } + + // deserialize json to krb_ticket_info object + Json::Value root; + std::ifstream json_file( metadata_file ); + + if ( json_file.is_open() ) + { + json_file >> root; + json_file.close(); + + // deserialize json to krb_ticket_info object + const Json::Value& child_tree_krb_info = root["krb_ticket_info"]; + + for ( const Json::Value& krb_info : child_tree_krb_info ) + { + creds_fetcher::krb_ticket_info* krb_ticket_info = + new creds_fetcher::krb_ticket_info; + std::string krb_file_path = krb_info["krb_file_path"].asString(); + + if ( contains_invalid_characters( krb_file_path ) ) + { + fprintf( stderr, SD_CRIT "krb file path contains invalid characters" ); + free( krb_ticket_info ); + break; + } + + // only add path if it exists + if ( std::filesystem::exists( krb_file_path ) ) + { + krb_ticket_info->krb_file_path = krb_file_path; + krb_ticket_info->service_account_name = + krb_info["service_account_name"].asString(); + krb_ticket_info->domain_name = krb_info["domain_name"].asString(); + krb_ticket_info->domainless_user = krb_info["domainless_user"].asString(); + + krb_ticket_info_list.push_back( krb_ticket_info ); + } + } + } + } + catch ( const std::exception& ex ) + { + std::cout << "Exception: '" << ex.what() << "'!" << std::endl; + fprintf( stderr, SD_CRIT "meta data file is not properly formatted" ); + return krb_ticket_info_list; + } + + return krb_ticket_info_list; + +} + /** * read the kerberos ticket information from the cache * @param file_path - file path for the metadata associated to a lease @@ -34,6 +111,7 @@ bool contains_invalid_characters( const std::string& path ) std::list read_meta_data_json( std::string file_path ) { std::list krb_ticket_info_list; + try { if ( file_path.empty() ) diff --git a/metadata/tests/metadata_sample.json b/metadata/tests/metadata_sample.json index 0f88559f..2f8d080e 100644 --- a/metadata/tests/metadata_sample.json +++ b/metadata/tests/metadata_sample.json @@ -1,16 +1,16 @@ { "krb_ticket_info": [ { - "krb_file_path": "\/usr\/share\/credentials-fetcher\/krbdir\/73099acdb5807b4bbf91\/ccname_WebApp01_7K4PEM", + "krb_file_path": "\/tmp\/var\/credentials-fetcher\/krbdir\/73099acdb5807b4bbf91\/ccname_WebApp01_7K4PEM", "service_account_name": "WebApp01", "domain_name": "contoso.com", "domainless_user": "user1" }, { - "krb_file_path": "\/usr\/share\/credentials-fetcher\/krbdir\/73099acdb5807b4bbf91\/ccname_WebApp03_53Yg4I", + "krb_file_path": "\/tmp\/var\/credentials-fetcher\/krbdir\/73099acdb5807b4bbf91\/ccname_WebApp03_53Yg4I", "service_account_name": "WebApp03", "domain_name": "contoso.com", "domainless_user": "user2" } ] -} \ No newline at end of file +} diff --git a/metadata/tests/metadata_test.cpp b/metadata/tests/metadata_test.cpp index 217e19ce..ae667e52 100644 --- a/metadata/tests/metadata_test.cpp +++ b/metadata/tests/metadata_test.cpp @@ -4,11 +4,21 @@ int read_meta_data_json_test() { - std::string metadata_file_path = "metadata_sample.json"; + std::string metadata_file_path = "/usr/sbin/credentials_fetcher_metadata_sample.json"; + std::string metadata_alt_file_path = "metadata_sample.json"; + std::string krb_files_dir = std::string("/tmp/") + CF_KRB_DIR + std::string("/"); + std::string logging_dir = std::string("/tmp/") + CF_LOGGING_DIR + std::string("/"); + + std::string cmd = "mkdir -p " + krb_files_dir + " " + logging_dir; + FILE* pFile = popen( cmd.c_str(), "r" ); + if ( pFile == nullptr ) + { + return EXIT_FAILURE; + } std::vector paths = { - "/usr/share/credentials-fetcher/krbdir/73099acdb5807b4bbf91/ccname_WebApp01_7K4PEM", - "/usr/share/credentials-fetcher/krbdir/73099acdb5807b4bbf91/ccname_WebApp03_53Yg4I" }; + krb_files_dir + std::string("73099acdb5807b4bbf91/ccname_WebApp01_7K4PEM"), + krb_files_dir + std::string("73099acdb5807b4bbf91/ccname_WebApp03_53Yg4I") }; for ( auto file_path : paths ) { @@ -23,7 +33,9 @@ int read_meta_data_json_test() } } - std::list result = read_meta_data_json( metadata_file_path ); + std::list result + = read_meta_data_json( metadata_file_path, metadata_alt_file_path, + krb_files_dir + std::string("73099acdb5807b4bbf91/ccname_WebApp01_7K4PEM") ); if ( result.empty() || result.size() != 2 ) { @@ -61,15 +73,17 @@ int read_meta_data_invalid_json_test() int write_meta_data_json_test() { std::string metadata_file_path = "metadata_sample.json"; + std::string krb_files_dir = std::string("/tmp/") + CF_KRB_DIR + std::string("/"); + std::string logging_dir = std::string("/tmp/") + CF_LOGGING_DIR + std::string("/"); std::vector paths = { - "/usr/share/credentials-fetcher/krbdir/73099acdb5807b4bbf91/ccname_WebApp01_7K4PEM", - "/usr/share/credentials-fetcher/krbdir/73099acdb5807b4bbf91/ccname_WebApp03_53Yg4I" }; + "73099acdb5807b4bbf91/ccname_WebApp01_7K4PEM", + "73099acdb5807b4bbf91/ccname_WebApp03_53Yg4I" }; for ( auto file_path : paths ) { // create the meta file in the lease directory - std::filesystem::path dirPath( file_path ); + std::filesystem::path dirPath( krb_files_dir + file_path ); std::filesystem::create_directories( dirPath.parent_path() ); if ( !std::filesystem::exists( file_path ) ) @@ -82,7 +96,6 @@ int write_meta_data_json_test() std::list test_ticket_info = read_meta_data_json( metadata_file_path ); - std::string krb_files_dir = "/usr/share/credentials-fetcher/krbdir"; std::string test_lease_id = "test1234567890"; int result = write_meta_data_json( test_ticket_info, test_lease_id, krb_files_dir ); diff --git a/package/credentials-fetcher.spec b/package/credentials-fetcher-no-grpc.spec similarity index 77% rename from package/credentials-fetcher.spec rename to package/credentials-fetcher-no-grpc.spec index 2ac802e2..b30052b9 100644 --- a/package/credentials-fetcher.spec +++ b/package/credentials-fetcher-no-grpc.spec @@ -5,7 +5,7 @@ # For handling bump release by rpmdev-bumpspec and mass rebuild %global baserelease 3 -Name: credentials-fetcher +Name: credentials-fetcher-no-grpc Version: %{major_version}.%{minor_version}.%{patch_version} Release: %{baserelease}%{?dist} Summary: credentials-fetcher is a daemon that refreshes tickets or tokens periodically @@ -14,11 +14,11 @@ License: Apache-2.0 URL: https://github.com/aws/credentials-fetcher Source0: https://github.com/aws/credentials-fetcher/archive/refs/tags/v.%{version}.tar.gz -BuildRequires: cmake3 make chrpath openldap-clients grpc-devel gcc-c++ glib2-devel jsoncpp-devel -BuildRequires: openssl-devel zlib-devel protobuf-devel re2-devel krb5-devel systemd-devel -BuildRequires: systemd-rpm-macros dotnet-sdk-6.0 grpc-plugins +BuildRequires: make chrpath openldap-clients jsoncpp-devel +BuildRequires: openssl-devel zlib-devel krb5-devel systemd-devel +BuildRequires: dotnet-sdk-6.0 -Requires: bind-utils openldap openldap-clients awscli dotnet-runtime-6.0 jsoncpp-devel jsoncpp +Requires: bind-utils openldap openldap-clients awscli dotnet-runtime-6.0 jsoncpp-devel jsoncpp realmd krb5-workstation openldap openldap-clients cyrus-sasl-gssapi # No one likes you i686 ExclusiveArch: x86_64 aarch64 s390x @@ -40,20 +40,35 @@ sed -r -i 's/(std=c\+\+)11/\117/' CMakeLists.txt %build %cmake3 -%cmake_build +#%cmake_build +make %install -%cmake_install +#%cmake_install +# Replace %cmake_install with explicit installation commands +mkdir -p %{buildroot}/%{_sbindir} +install -m 0755 credentials-fetcherd %{buildroot}/%{_sbindir}/credentials-fetcherd + +install -m 0755 credentials_fetcher_utf16_private.exe %{buildroot}/%{_sbindir}/credentials_fetcher_utf16_private.exe +install -m 0755 credentials_fetcher_utf16_private.runtimeconfig.json %{buildroot}/%{_sbindir}/credentials_fetcher_utf16_private.runtimeconfig.json +install -m 0755 metadata_sample.json %{buildroot}/%{_sbindir}/credentials_fetcher_metadata_sample.json +install -m 0755 sample_credspec.json %{buildroot}/%{_sbindir}/credentials_fetcher_sample_credspec.json + + +mkdir -p %{buildroot}/%{_unitdir} +install -m 0755 scripts/systemd/credentials-fetcher.service %{buildroot}/%{_unitdir}/credentials-fetcher.service + # https://docs.fedoraproject.org/en-US/packaging-guidelines/#_removing_rpath # https://docs.fedoraproject.org/en-US/packaging-guidelines/#_rpath_for_internal_libraries chrpath --delete %{buildroot}/%{_sbindir}/credentials-fetcherd %check -# TBD: Run tests from top-level directory -ctest3 +CF_CRED_SPEC_FILE=%{buildroot}/%{_sbindir}/credentials_fetcher_sample_credspec.json ctest3 -V %{buildroot}/%{_sbindir}/credentials-fetcherd -t %files %{_sbindir}/credentials-fetcherd +%{_sbindir}/credentials_fetcher_metadata_sample.json +%{_sbindir}/credentials_fetcher_sample_credspec.json %{_unitdir}/credentials-fetcher.service %license LICENSE # https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/ @@ -126,3 +141,4 @@ ctest3 - Fixes to rpm spec * Mon Jun 6 2022 Samiullah Mohammed - 0.0.1 - Initial commit + diff --git a/sample_credspec/CMakeLists.txt b/sample_credspec/CMakeLists.txt new file mode 100644 index 00000000..24e51020 --- /dev/null +++ b/sample_credspec/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.10) + +project(sample_credspec) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") + +FILE(GLOB SRC_FILES src/*.cpp tests/*.cpp) +set(sample_credspec "${SRC_FILES}" PARENT_SCOPE) + +# test sample for unit tests +file( + COPY ${CMAKE_CURRENT_SOURCE_DIR}/sample_credspec.json + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../ + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE +) + +enable_testing () diff --git a/sample_credspec/sample_credspec.json b/sample_credspec/sample_credspec.json new file mode 100644 index 00000000..7f67ee7e --- /dev/null +++ b/sample_credspec/sample_credspec.json @@ -0,0 +1 @@ +{"CmsPlugins":["ActiveDirectory"],"DomainJoinConfig":{"Sid":"S-1-5-21-4217655605-3681839426-3493040985","MachineAccountName":"WebApp01","Guid":"af602f85-d754-4eea-9fa8-fd76810485f1","DnsTreeName":"contoso.com","DnsName":"contoso.com","NetBiosName":"contoso"},"ActiveDirectoryConfig":{"GroupManagedServiceAccounts":[{"Name":"WebApp01","Scope":"customertest.local"},{"Name":"WebApp01","Scope":"customertest.local"}]}} diff --git a/sample_credspec/sample_credspec2.json b/sample_credspec/sample_credspec2.json new file mode 100644 index 00000000..1e4caa38 --- /dev/null +++ b/sample_credspec/sample_credspec2.json @@ -0,0 +1,2 @@ +{"CmsPlugins":["ActiveDirectory"],"DomainJoinConfig":{"Sid":"S-1-5-21-588757487-522635677-1531879567","MachineAccountName":"gmsaeks","Guid":"8a13e3a1-9f2a-4431-baf7-9f72d9135554","DnsTreeName":"example.com","DnsName":"example.com","NetBiosName":"example"},"ActiveDirectoryConfig":{"GroupManagedServiceAccounts":[{"Name":"gmsaeks","Scope":"example.com"},{"Name":"gmsaeks","Scope":"example"}]}} + diff --git a/scripts/systemd/credentials-fetcher.service b/scripts/systemd/credentials-fetcher.service index 8bd4b14e..6c01dd0c 100644 --- a/scripts/systemd/credentials-fetcher.service +++ b/scripts/systemd/credentials-fetcher.service @@ -3,12 +3,9 @@ Description=credentials-fetcher systemd service unit file. [Service] ExecStartPre=mkdir -p /var/credentials-fetcher/krbdir /var/credentials-fetcher/socket /var/credentials-fetcher/logging -ExecStartPre=chgrp ec2-user /var/credentials-fetcher /var/credentials-fetcher/krbdir /var/credentials-fetcher/socket /var/credentials-fetcher/logging -ExecStartPre=chmod 755 /var/credentials-fetcher /var/credentials-fetcher/krbdir /var/credentials-fetcher/socket /var/credentials-fetcher/logging ExecStart=/usr/sbin/credentials-fetcherd -ExecStartPost=chgrp ec2-user /var/credentials-fetcher/socket/credentials_fetcher.sock -ExecStartPost=chmod 660 /var/credentials-fetcher/socket/credentials_fetcher.sock Environment="CREDENTIALS_FETCHERD_STARTED_BY_SYSTEMD=1" +Environment="CF_CRED_SPEC_FILE=/var/credentials-fetcher/credspec.json" Type=notify NotifyAccess=main WatchdogSec=5s