common/util.hpp (773 lines of code) (raw):

#include "constants.h" #include "daemon.h" #include <cstdio> #include <fstream> #include <iostream> #include <openssl/crypto.h> #include <string> #include <sys/stat.h> #include <utility> extern const std::vector<char> invalid_characters; extern const std::string install_path_for_decode_exe; extern const std::string install_path_for_aws_cli; class Util { public: /** * Check if binary is writable other than root * @param filename - must be owned and writable only by root * @return - true or false */ static bool check_file_permissions( std::string filename ) { struct stat st; if ( lstat( filename.c_str(), &st ) == -1 ) { return false; } // S_IWOTH - Write permission bit for other users. Usually 02. if ( ( st.st_uid != 0 ) || ( st.st_gid != 0 ) || ( st.st_mode & S_IWOTH ) ) { return false; } return true; } static std::pair<int, std::string> check_util_binaries_permissions() { std::pair<int, std::string> result; std::pair<int, std::string> cmd = Util::exec_shell_cmd( "which kinit" ); Util::rtrim( cmd.second ); if ( !Util::check_file_permissions( cmd.second ) ) { std::cerr << Util::getCurrentTime() << '\t' << "ERROR: kinit not found" << std::endl; result = std::make_pair( -1, std::string( "ERROR:: kinit not found" ) ); return result; } cmd = Util::exec_shell_cmd( "which ldapsearch" ); Util::rtrim( cmd.second ); if ( !Util::check_file_permissions( cmd.second ) ) { std::cerr << Util::getCurrentTime() << '\t' << "ERROR: ldapsearch not found" << std::endl; result = std::make_pair( -1, "ERROR:: ldapsearch not found" ); return result; } if ( !Util::check_file_permissions( std::string( install_path_for_decode_exe ) ) ) { result = std::make_pair( -1, "ERROR:: decode.exe not found" ); return result; } if ( !Util::check_file_permissions( std::string( install_path_for_aws_cli ) ) ) { result = std::make_pair( -1, "ERROR:: AWS CLI not found" ); return result; } return std::make_pair( 0, "" ); } /** * Execute a shell command such as "ls /tmp/" * output is a pair of error code and output log * @param cmd - command to be executed in shell * @return result pair(error-code, output log of shell execution) */ static std::pair<int, std::string> exec_shell_cmd( std::string cmd ) { std::string output; char line[80]; char* cmd_str = (char*)calloc( cmd.length() + 1, sizeof( char ) ); strncpy( cmd_str, cmd.c_str(), cmd.length() ); FILE* pFile = popen( cmd_str, "r" ); if ( pFile == nullptr ) { std::pair<int, std::string> result = std::make_pair( -1, std::string( "" ) ); free( cmd_str ); return result; } while ( fgets( line, sizeof( line ), pFile ) != nullptr ) { output += std::string( line ); } int error_code = pclose( pFile ); std::pair<int, std::string> result = std::make_pair( error_code, output ); free( cmd_str ); return result; } static std::pair<int, std::string> get_realm_name() { std::pair<int, std::string> result; std::pair<int, std::string> realm_name_result = exec_shell_cmd( "realm list | grep 'realm-name' | cut -f2 -d: | tr -d ' ' | tr -d '\n'" ); if ( realm_name_result.first != 0 ) { result.first = realm_name_result.first; realm_name_result = exec_shell_cmd( "net ads info | grep 'Realm' | cut -f2 -d: | tr -d ' ' | tr -d '\n'" ); if ( realm_name_result.first != 0 ) { result.first = realm_name_result.first; return result; } } return realm_name_result; } static std::pair<int, std::string> check_domain_name( std::string domain_name ) { std::pair<int, std::string> domain_name_result = exec_shell_cmd( "realm list | grep 'domain-name' | cut -f2 -d: | tr -d ' ' | tr -d '\n'" ); if ( domain_name_result.first != 0 || ( not std::equal( domain_name_result.second.begin(), domain_name_result.second.end(), domain_name.begin() ) ) ) { domain_name_result.first = -1; return domain_name_result; } return domain_name_result; } static std::pair<int, std::string> is_hostname_cmd_present() { std::pair<int, std::string> cmd = exec_shell_cmd( "which hostname" ); rtrim( cmd.second ); if ( !check_file_permissions( cmd.second ) ) { std::pair<int, std::string> result = std::make_pair( -1, std::string( "ERROR: hostname not found" ) ); return result; } return cmd; } static std::pair<int, std::string> is_realm_cmd_present() { std::pair<int, std::string> cmd = exec_shell_cmd( "which realm" ); rtrim( cmd.second ); if ( !check_file_permissions( cmd.second ) ) { std::cerr << getCurrentTime() << '\t' << "ERROR: realm not found" << std::endl; std::pair<int, std::string> result = std::make_pair( -1, std::string( "ERROR: realm not found" ) ); return result; } return cmd; } static std::pair<int, std::string> is_kinit_cmd_present() { std::pair<int, std::string> cmd = exec_shell_cmd( "which kinit" ); rtrim( cmd.second ); if ( !check_file_permissions( cmd.second ) ) { std::cerr << getCurrentTime() << '\t' << "ERROR: kinit not found" << std::endl; std::pair<int, std::string> result = std::make_pair( -1, std::string( "ERROR: kinit not found" ) ); return result; } return cmd; } static std::pair<int, std::string> is_ldapsearch_cmd_present() { std::pair<int, std::string> cmd = exec_shell_cmd( "which ldapsearch" ); rtrim( cmd.second ); if ( !check_file_permissions( cmd.second ) ) { std::cerr << getCurrentTime() << '\t' << "ERROR: ldapsearch not found" << std::endl; std::pair<int, std::string> result = std::make_pair( -1, std::string( "ERROR: ldapsearch not found" ) ); return result; } return cmd; } static std::pair<int, std::string> is_decode_exe_present() { std::pair<int, std::string> result; if ( !check_file_permissions( install_path_for_decode_exe ) ) { std::pair<int, std::string> result = std::make_pair( -1, std::string( "ERROR: decode.exe not found" ) ); return result; } result = std::make_pair( 0, "" ); return result; } static std::string retrieve_variable_from_ecs_config( std::string ecs_variable_name ) { const char* ecs_config_file_name = "/etc/ecs/ecs.config"; std::ifstream config_file( ecs_config_file_name ); std::string line; std::vector<std::string> results; while ( std::getline( config_file, line ) ) { results = Util::split_string( line, '=' ); if ( results.empty() || results.size() != 2 ) { std::cerr << Util::getCurrentTime() << '\t' << "invalid configuration format" << std::endl; return ""; } std::string key = results[0]; std::string value = results[1]; Util::rtrim( key ); Util::ltrim( value ); if ( key.compare( ENV_CF_GMSA_OU ) == 0 && ecs_variable_name.compare( key ) == 0 ) { return value; } if ( key.compare( ENV_CF_GMSA_SECRET_NAME ) == 0 && ecs_variable_name.compare( key ) == 0 ) { return value; } if ( key.compare( ENV_CF_DOMAIN_CONTROLLER ) == 0 && ecs_variable_name.compare( key ) == 0 ) { return value; } if ( key.compare( ENV_CF_DISTINGUISHED_NAME ) == 0 && ecs_variable_name.compare( key ) == 0 ) { return value; } } return ""; } static Json::Value get_secret_from_secrets_manager( std::string aws_sm_secret_name ) { Json::Value root = Json::nullValue; std::string command = std::string( 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::pair<int, std::string> result = Util::exec_shell_cmd( command ); if ( result.first == 0 ) { // deserialize json to krb_ticket_info object Json::CharReaderBuilder reader; std::istringstream string_stream( result.second ); std::string errors; Json::parseFromStream( reader, string_stream, &root, &errors ); } return root; } static std::pair<int, std::string> get_base_dn_from_secret( std::string secret_name ) { std::pair<int, std::string> result = std::make_pair( -1, "" ); std::string distinguished_name; if ( !secret_name.empty() ) { Json::Value root = get_secret_from_secrets_manager( secret_name ); distinguished_name = root["distinguishedName"].asString(); if ( distinguished_name.empty() ) { distinguished_name = root["distinguishedNameOfgMSA"].asString(); } if ( !distinguished_name.empty() ) { result.first = 0; } result.second = distinguished_name; } return result; } static std::vector<std::string> get_FQDN_list( std::string domain_name ) { std::string domain_controller_gmsa( ENV_CF_DOMAIN_CONTROLLER ); std::string fqdn_from_env_var; fqdn_from_env_var = Util::retrieve_variable_from_ecs_config( domain_controller_gmsa ); std::vector<std::string> fqdn_list; if ( fqdn_from_env_var.empty() ) { fqdn_list = Util::get_FQDNs( domain_name ); for ( auto fqdn : fqdn_list ) { std::cerr << "Found ldap._tcp.dc._msdcs DNS controller " << fqdn << std::endl; } } else { fqdn_list.push_back( fqdn_from_env_var ); } return fqdn_list; } /** * UTF-16 diagnostic: Test utf16 capability * @return - true (pass) or false (fail) */ static int test_utf16_decode() { const char* test_msds_managed_password = "msDS-ManagedPassword:: " "AQAAACIBAAAQAAAAEgEaAciMhCofvo1R4kkVYm79aRysUcOs7NhhHvO" "exhNTV9KXAn1v8AYMN1lMC/V6W0dZVrQRpGZ/EvWi33Lq2xoR5ANuJf623JQRj3pMZQBqQLRjRoPn" "UJYY8H74aVysf0t+1M0moLkm0IPSCB52Mm0CC9flTT0D9KZV2Mvf4FpgvYpYoOQvUmd0UOV72Tk/d" "leM8zTWjRL5ccfzwt5p8akMEl6W0RPj1pDbqxtbpJFQiLQd7HRlSkYPeBKDB9r6CItrQTo8j+pgJf" "B4+wVbOUZuMXrKkDVh8XUOUBdGhznntRWnDM2DhwBoFEisBr133Vo8aRcedYqwNj/LEsrimEJaeuY" "AAAQCCBrPFgAABKQ3Z84WAAA= #"; uint8_t test_password_buf[1024]; const uint8_t test_gmsa_utf8_password[] = { 0xE8, 0xB3, 0x88, 0xE2, 0xAA, 0x84, 0xEB, 0xB8, 0x9F, 0xE5, 0x86, 0x8D, 0xE4, 0xA7, 0xA2, 0xE6, 0x88, 0x95, 0xEF, 0xB5, 0xAE, 0xE1, 0xB1, 0xA9, 0xE5, 0x86, 0xAC, 0xEA, 0xB3, 0x83, 0xEF, 0xBF, 0xBD, 0xE1, 0xB9, 0xA1, 0xE9, 0xBB, 0xB3, 0xE1, 0x8F, 0x86, 0xE5, 0x9D, 0x93, 0xE9, 0x9F, 0x92, 0xE7, 0xB4, 0x82, 0xEF, 0x81, 0xAF, 0xE0, 0xB0, 0x86, 0xE5, 0xA4, 0xB7, 0xE0, 0xAD, 0x8C, 0xE7, 0xAB, 0xB5, 0xE4, 0x9D, 0x9B, 0xE5, 0x99, 0x99, 0xE1, 0x86, 0xB4, 0xE6, 0x9A, 0xA4, 0xE1, 0x89, 0xBF, 0xEA, 0x8B, 0xB5, 0xE7, 0x8B, 0x9F, 0xEF, 0xBF, 0xBD, 0xE1, 0x84, 0x9A, 0xCF, 0xA4, 0xE2, 0x95, 0xAE, 0xEB, 0x9B, 0xBE, 0xE9, 0x93, 0x9C, 0xE8, 0xBC, 0x91, 0xE4, 0xB1, 0xBA, 0x65, 0xE4, 0x81, 0xAA, 0xE6, 0x8E, 0xB4, 0xE8, 0x8D, 0x86, 0xE5, 0x83, 0xA7, 0xE1, 0xA2, 0x96, 0xE7, 0xBB, 0xB0, 0xE6, 0xA7, 0xB8, 0xEA, 0xB1, 0x9C, 0xE4, 0xAD, 0xBF, 0xED, 0x91, 0xBE, 0xE2, 0x9B, 0x8D, 0xEB, 0xA6, 0xA0, 0xED, 0x80, 0xA6, 0xED, 0x8A, 0x83, 0xE1, 0xB8, 0x88, 0xE3, 0x89, 0xB6, 0xC9, 0xAD, 0xED, 0x9C, 0x8B, 0xE4, 0xB7, 0xA5, 0xCC, 0xBD, 0xEA, 0x9B, 0xB4, 0xF0, 0xA5, 0x9F, 0x8B, 0xE5, 0xAB, 0xA0, 0xEB, 0xB5, 0xA0, 0xE5, 0xA2, 0x8A, 0xEE, 0x92, 0xA0, 0xE5, 0x88, 0xAF, 0xE7, 0x91, 0xA7, 0xEE, 0x95, 0x90, 0xEF, 0xBF, 0xBD, 0xE3, 0xBC, 0xB9, 0xE5, 0x9D, 0xB6, 0xEF, 0x8E, 0x8C, 0xED, 0x98, 0xB4, 0xE1, 0x8A, 0x8D, 0xE7, 0x87, 0xB9, 0xEF, 0x8F, 0x87, 0xEF, 0xBF, 0xBD, 0xEF, 0x85, 0xA9, 0xE0, 0xB2, 0xA9, 0xE5, 0xB8, 0x92, 0xED, 0x86, 0x96, 0xEE, 0x8C, 0x93, 0xE9, 0x83, 0x96, 0xEA, 0xAF, 0x9B, 0xE5, 0xAC, 0x9B, 0xE9, 0x86, 0xA4, 0xE8, 0xA1, 0x90, 0xE1, 0xB6, 0xB4, 0xE7, 0x93, 0xAC, 0xE4, 0xA9, 0xA5, 0xE0, 0xBD, 0x86, 0xE1, 0x89, 0xB8, 0xDE, 0x83, 0xEF, 0xAB, 0x9A, 0xE8, 0xAC, 0x88, 0xE4, 0x85, 0xAB, 0xE3, 0xB0, 0xBA, 0xEE, 0xAA, 0x8F, 0xE2, 0x95, 0xA0, 0xE7, 0xA3, 0xB0, 0xD7, 0xBB, 0xE3, 0xA5, 0x9B, 0xE6, 0xB9, 0x86, 0xE7, 0xA8, 0xB1, 0xE9, 0x83, 0x8A, 0xE6, 0x84, 0xB5, 0xE7, 0x97, 0xB1, 0xE5, 0x80, 0x8E, 0xE4, 0x98, 0x97, 0xE3, 0xA6, 0x87, 0xEB, 0x97, 0xA7, 0xEA, 0x9C, 0x95, 0xEC, 0xB4, 0x8C, 0xE8, 0x9E, 0x83, 0xE6, 0xA0, 0x80, 0xE4, 0xA0, 0x94, 0xDA, 0xAC, 0xE7, 0x9E, 0xBD, 0xE5, 0xAB, 0x9D, 0xE6, 0xA4, 0xBC, 0xE1, 0xB8, 0x97, 0xE8, 0xA9, 0xB5, 0xE3, 0x9A, 0xB0, 0xEC, 0xAC, 0xBF, 0xEC, 0xA8, 0x92, 0xE9, 0xA3, 0xA2, 0xE5, 0xA9, 0x82, 0xEE, 0x99, 0xBA }; std::string decoded_password_file = "./decoded_password_file"; std::pair<size_t, void*> base64_decoded_password_blob = find_password( test_msds_managed_password ); if ( base64_decoded_password_blob.first == 0 || base64_decoded_password_blob.second == nullptr ) { return EXIT_FAILURE; } struct stat st; std::string decode_exe_path; if ( stat( install_path_for_decode_exe.c_str(), &st ) == 0 ) { decode_exe_path = install_path_for_decode_exe; } else { // For test during rpmbuild decode_exe_path = "./decode.exe"; } // Use decode.exe in build directory std::string decode_cmd = decode_exe_path + std::string( " > " ) + decoded_password_file; blob_t* blob = ( (blob_t*)base64_decoded_password_blob.second ); FILE* fp = popen( decode_cmd.c_str(), "w" ); if ( fp == nullptr ) { std::cerr << Util::getCurrentTime() << '\t' << "Self test failed" << std::endl; OPENSSL_cleanse( base64_decoded_password_blob.second, base64_decoded_password_blob.first ); OPENSSL_free( base64_decoded_password_blob.second ); return EXIT_FAILURE; } fwrite( blob->current_password, 1, GMSA_PASSWORD_SIZE, fp ); if ( pclose( fp ) < 0 ) { std::cerr << Util::getCurrentTime() << '\t' << "Self test failed" << std::endl; OPENSSL_cleanse( base64_decoded_password_blob.second, base64_decoded_password_blob.first ); OPENSSL_free( base64_decoded_password_blob.second ); return EXIT_FAILURE; } fp = fopen( decoded_password_file.c_str(), "rb" ); if ( fp == nullptr ) { std::cerr << Util::getCurrentTime() << '\t' << "Self test failed" << std::endl; OPENSSL_cleanse( base64_decoded_password_blob.second, base64_decoded_password_blob.first ); OPENSSL_free( base64_decoded_password_blob.second ); return EXIT_FAILURE; } fread( test_password_buf, 1, GMSA_PASSWORD_SIZE, fp ); if ( memcmp( test_gmsa_utf8_password, test_password_buf, GMSA_PASSWORD_SIZE ) == 0 ) { // utf16->utf8 conversion works as expected std::cerr << Util::getCurrentTime() << '\t' << "Self test is successful" << std::endl; OPENSSL_cleanse( base64_decoded_password_blob.second, base64_decoded_password_blob.first ); OPENSSL_free( base64_decoded_password_blob.second ); unlink( decoded_password_file.c_str() ); return EXIT_SUCCESS; } std::cerr << Util::getCurrentTime() << '\t' << "Self test failed" << std::endl; OPENSSL_cleanse( base64_decoded_password_blob.second, base64_decoded_password_blob.first ); OPENSSL_free( base64_decoded_password_blob.second ); unlink( decoded_password_file.c_str() ); return EXIT_FAILURE; } /** * base64_decode - Decodes base64 encoded string * @param password - base64 encoded password * @param base64_decode_len - Length after decode * @return buffer with base64 decoded contents */ static uint8_t* base64_decode( const std::string& password, gsize* base64_decode_len ) { if ( base64_decode_len == nullptr || password.empty() ) { return nullptr; } *base64_decode_len = 0; guchar* result = g_base64_decode( password.c_str(), base64_decode_len ); if ( result == nullptr || *base64_decode_len <= 0 ) { return nullptr; } void* secure_mem = OPENSSL_malloc( *base64_decode_len ); if ( secure_mem == nullptr ) { g_free( result ); return nullptr; } memcpy( secure_mem, result, *base64_decode_len ); memset( result, 0, *base64_decode_len ); g_free( result ); /** * secure_mem must be freed later */ return (uint8_t*)secure_mem; } static std::pair<int, std::string> get_base_dn( std::string domain_name ) { if ( !domain_name.empty() ) { // DC=Contoso,DC=com std::string base_dn = ""; auto results = Util::split_string( domain_name, '.' ); for ( auto& result : results ) { base_dn += "DC=" + result + ","; } base_dn.pop_back(); // Remove last comma return std::make_pair( 0, base_dn ); } return std::make_pair( -1, "" ); } static std::pair<int, std::string> find_dn( std::string gmsa_account_name, std::string base_dn, std::string fqdn ) { /** * ldapsearch -H ldap://ip-xxxxxxxx.activedirectory1.com * -b 'DC=ActiveDirectory1,DC=com' -s sub '(CN=WebApp01)' distinguishedName | grep * "distinguishedName:" */ std::string distinguished_name; std::string search_string = " -s sub '(CN=" + gmsa_account_name + ")' distinguishedName"; std::pair<int, std::string> ldap_search_result = Util::execute_ldapsearch( gmsa_account_name, base_dn, fqdn, search_string ); if ( ldap_search_result.first == 0 && !ldap_search_result.second.empty() ) { std::size_t start_pos = ldap_search_result.second.find( "distinguishedName:" ); if ( start_pos != std::string::npos ) { // distinguishedName: // CN=WebApp01,OU=MYOU,OU=Users,OU=ActiveDirectory,DC=ActiveDirectory1,DC=com distinguished_name = "distinguishedName: "; start_pos += distinguished_name.length(); distinguished_name = ldap_search_result.second.substr( start_pos ); std::size_t end_pos = distinguished_name.find_first_of( "\n" ); if ( end_pos != std::string::npos ) { distinguished_name = distinguished_name.substr( 0, end_pos ); } else { distinguished_name = ""; return std::make_pair( -1, distinguished_name ); } } } else { distinguished_name = ""; return std::make_pair( -1, distinguished_name ); } return std::make_pair( 0, distinguished_name ); } static std::pair<size_t, void*> find_password( std::string ldap_search_result ) { size_t base64_decode_len = 0; std::vector<std::string> results; std::string password = std::string( "msDS-ManagedPassword::" ); results = Util::split_string( ldap_search_result, '#' ); bool password_found = false; for ( auto& result : results ) { auto found = result.find( password ); if ( found != std::string::npos ) { found += password.length(); password = result.substr( found + 1, result.length() ); // std::cerr << "Password = " << password << std::endl; password_found = true; break; } } uint8_t* blob_base64_decoded = nullptr; if ( password_found ) { blob_base64_decoded = base64_decode( password, &base64_decode_len ); if ( blob_base64_decoded == nullptr ) { std::cerr << Util::getCurrentTime() << '\t' << "ERROR: base64 buffer is null" << std::endl; return std::make_pair( 0, nullptr ); } } return std::make_pair( base64_decode_len, blob_base64_decoded ); } static std::pair<int, std::string> execute_ldapsearch( std::string gmsa_account_name, std::string distinguished_name, std::string fqdn, std::string search_string ) { std::string cmd; std::pair<int, std::string> ldap_search_result; // -N: Do not use reverse DNS to canonicalize SASL host name. // With this flag, ldapsearch uses the IP address directly for identification purposes, rather than trying to resolve it to a hostname. cmd = std::string( "ldapsearch -o ldif_wrap=no -LLL -Y GSSAPI -H ldap://" ) + fqdn; cmd += std::string( " -b '" ) + distinguished_name + std::string( "' " ) + search_string; cmd += std::string( " -N" ); std::cerr << Util::getCurrentTime() << '\t' << "INFO: " << cmd << std::endl; std::cerr << cmd << std::endl; for ( int i = 0; i < 2; i++ ) { ldap_search_result = Util::exec_shell_cmd( cmd ); cmd += ldap_search_result.second; ldap_search_result.second = cmd; // Add retry, ldapsearch seems to fail and then succeed on retry if ( ldap_search_result.first != 0 ) { std::string err_msg = std::string( "ERROR: ldapsearch failed with FQDN = " ) + fqdn; std::cerr << err_msg << std::endl; err_msg = Util::getCurrentTime() + std::string( "ERROR: ldapsearch failed to get gMSA credentials: " + ldap_search_result.second ); std::cerr << err_msg << std::endl; err_msg = ldap_search_result.second + err_msg; ldap_search_result.second = err_msg; } else { std::string err_msg = "INFO: ldapsearch succeeded with FQDN = "; std::cerr << err_msg << fqdn << std::endl; ldap_search_result.first = 0; ldap_search_result.second = ldap_search_result.second + err_msg; break; } } return ldap_search_result; } // Remove trailing characters in FQDN returned by dig command static std::string remove_trailing_dot_and_newline( std::string arg ) { if ( arg.back() == '\n' || arg.back() == '\r' ) { arg.pop_back(); } if ( arg.back() == '.' ) { arg.pop_back(); } return arg; } static std::vector<std::string> get_FQDNs( std::string domain_name ) { /** * Find SRV record * "The SRV or "service locator" DNS record type enables service discovery in the DNS. * SRV records allow services to be advertised on specific ports and used in an order * controlled by the owner of the service. SRV also provides a load balancing feature." * https://www.nslookup.io/learning/dns-record-types/srv/ */ // https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/verify-srv-dns-records-have-been-created#method-3-use-nslookup std::string cmd = "nslookup -type=srv _ldap._tcp.dc._msdcs." + domain_name + " | grep " + domain_name + " | sed 's/^.* //g'"; std::pair<int, std::string> nslookup_output = Util::exec_shell_cmd( cmd ); std::vector<std::string> fqdns; if ( nslookup_output.first == 0 ) { fqdns = split_string( nslookup_output.second, '\n' ); for ( auto& fqdn : fqdns ) { if ( fqdn.back() == '.' ) { fqdn.pop_back(); } } return fqdns; } else { cmd = "dig +short _ldap._tcp.dc._msdcs." + domain_name + " -t any | sed 's/^.* //g'"; nslookup_output = Util::exec_shell_cmd( cmd ); if ( nslookup_output.first == 0 ) { std::vector<std::string> fqdns = split_string( nslookup_output.second, '\n' ); for ( auto& fqdn : fqdns ) { if ( fqdn.back() == '.' ) { fqdn.pop_back(); } } return fqdns; } } return fqdns; } static std::pair<int, std::string> execute_kinit_in_domain_joined_case( std::string principal ) { // kinit -k 'EC2AMAZ-8L8GWS$@CONTOSO.COM' std::transform( principal.begin(), principal.end(), principal.begin(), []( unsigned char c ) { return std::toupper( c ); } ); std::string kinit_cmd = "kinit -kt /etc/krb5.keytab " + principal; std::pair<int, std::string> result = exec_shell_cmd( kinit_cmd ); return result; } /** * 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 */ static std::vector<std::string> split_string( std::string input_string, char delimiter ) { std::vector<std::string> results; std::istringstream input_string_stream( input_string ); std::string token; while ( std::getline( input_string_stream, token, delimiter ) ) { results.push_back( token ); if ( delimiter == '=' ) { while ( std::getline( input_string_stream, token ) ) { results.push_back( token ); } break; } } return results; } /** * trim from start (in place) * @param s - string input */ static void ltrim( std::string& s ) { s.erase( s.begin(), std::find_if( s.begin(), s.end(), []( unsigned char ch ) { return !std::isspace( ch ); } ) ); } /** * trim from end (in place) * @param s - string input */ static void rtrim( std::string& s ) { s.erase( std::find_if( s.rbegin(), s.rend(), []( unsigned char ch ) { return !std::isspace( ch ); } ) .base(), s.end() ); } /** * get current time */ static std::string getCurrentTime() { time_t now = time( 0 ); struct tm tstruct; char buf[80]; tstruct = *localtime( &now ); strftime( buf, sizeof( buf ), "%Y-%m-%d %X", &tstruct ); std::string curr_time = std::string( buf ); return curr_time; } /** clear string **/ static void clearString( std::string& str ) { if ( !str.empty() ) { // Use OPENSSL_cleanse to securely clear the memory OPENSSL_cleanse( &str[0], str.size() ); } // Clear the string content str.clear(); } /** * This function generates kerberos ticket with user credentials * User credentials must have adequate privileges to read gMSA passwords * This is an alternative to the machine credentials approach above * @param cf_daemon - parent daemon object * @return error-code - 0 if successful */ static std::pair<int, std::string> generate_krb_ticket_using_secret_vault( std::string domain_name, std::string aws_sm_secret_name, CF_logger& cf_logger ) { std::pair<int, std::string> result; result = Util::check_util_binaries_permissions(); if ( result.first != 0 ) { return result; } std::string username = ""; std::string password = ""; Json::Value root = Util::get_secret_from_secrets_manager( aws_sm_secret_name ); std::string distinguished_name = ""; if ( root != Json::nullValue ) { username = root["username"].asString(); if ( username.empty() ) { username = root["usernameOfStandardUserAccount"].asString(); } password = root["password"].asString(); if ( password.empty() ) { password = root["passwordOfStandardUserAccount"].asString(); } distinguished_name = root["distinguishedName"].asString(); if ( distinguished_name.empty() ) { distinguished_name = root["distinguishedNameOfgMSA"].asString(); } } else { return std::make_pair( -1, "ERROR: username and password not found in secret" ); } if ( !distinguished_name.empty() ) { std::string err_msg = "[Optional] DN from Secrets Manager = " + distinguished_name; std::cerr << err_msg << std::endl; cf_logger.logger( LOG_ERR, err_msg.c_str() ); } std::transform( domain_name.begin(), domain_name.end(), domain_name.begin(), []( unsigned char c ) { return std::toupper( c ); } ); // kinit using api interface char* kinit_argv[3]; kinit_argv[0] = (char*)"my_kinit"; username = username + "@" + domain_name; kinit_argv[1] = (char*)username.c_str(); kinit_argv[2] = (char*)password.c_str(); int ret = my_kinit_main( 2, kinit_argv ); #if 0 /* The old way */ std::string kinit_cmd = "echo '" + password + "' | kinit -V " + username + "@" + domain_name; username = "xxxx"; password = "xxxx"; result = Util::exec_shell_cmd( kinit_cmd ); kinit_cmd = "xxxx"; return result.first; #endif Util::clearString( username ); Util::clearString( password ); result = std::make_pair( ret, distinguished_name ); return result; } /** * This function generates kerberos ticket with user with access to gMSA password credentials * User credentials must have adequate privileges to read gMSA passwords * This is an alternative to the machine credentials approach above * @param cf_daemon - parent daemon object * @return error-code - 0 if successful */ static std::pair<int, std::string> generate_krb_ticket_using_username_and_password( std::string domain_name, std::string username, std::string password, CF_logger& cf_logger ) { std::pair<int, std::string> result; result = Util::is_kinit_cmd_present(); if ( result.first != 0 ) { cf_logger.logger( LOG_ERR, result.second.c_str() ); return result; } result = Util::is_ldapsearch_cmd_present(); if ( result.first != 0 ) { cf_logger.logger( LOG_ERR, result.second.c_str() ); return result; } std::transform( domain_name.begin(), domain_name.end(), domain_name.begin(), []( unsigned char c ) { return std::toupper( c ); } ); // kinit using api interface char* kinit_argv[3]; kinit_argv[0] = (char*)"my_kinit"; username = username + "@" + domain_name; kinit_argv[1] = (char*)username.c_str(); kinit_argv[2] = (char*)password.c_str(); int ret = my_kinit_main( 2, kinit_argv ); Util::clearString( username ); Util::clearString( password ); result = std::make_pair( ret, "" ); return result; } /** * If the host is domain-joined, the result is of the form EC2AMAZ-Q5VJZQ$@CONTOSO.COM' * @param domain_name: Expected domain name as per configuration * @return result pair<int, std::string> (error-code - 0 if successful * string of the form EC2AMAZ-Q5VJZQ$@CONTOSO.COM') */ static std::pair<int, std::string> get_machine_principal( std::string domain_name, CF_logger& cf_logger ) { std::pair<int, std::string> result = std::make_pair( -1, "" ); char hostname[HOST_NAME_MAX]; int status = gethostname( hostname, HOST_NAME_MAX ); if ( status ) { result.first = status; return result; } std::pair<int, std::string> realm_name_result = Util::get_realm_name(); if ( realm_name_result.first != 0 ) { return realm_name_result; } std::pair<int, std::string> domain_name_result = Util::check_domain_name( domain_name ); if ( domain_name_result.first != 0 ) { return domain_name_result; } std::string s = std::string( hostname ); std::string host_name = s.substr( 0, s.find( '.' ) ); // truncate the hostname to the host name size limit defined by microsoft if ( host_name.length() > HOST_NAME_LENGTH_LIMIT ) { std::string log_message = "WARNING: " + std::string( __func__ ) + " : " + std::to_string( __LINE__ ) + " hostname exceeds 15 characters, this can cause problems in " "getting kerberos tickets, please reduce hostname length"; cf_logger.logger( LOG_ERR, log_message.c_str() ); host_name = host_name.substr( 0, HOST_NAME_LENGTH_LIMIT ); std::cerr << Util::getCurrentTime() << '\t' << "INFO: hostname exceeds 15 characters this can " "cause problems in getting kerberos tickets, " "please reduce hostname length" << std::endl; } /** * Machine principal is of the format EC2AMAZ-Q5VJZQ$@CONTOSO.COM' */ result.first = 0; result.second = "'" + host_name + "$@'" + realm_name_result.second; return result; } /** * Checks if a string contains any invalid characters for Active Directory account names * * @param value The string to validate against invalid AD account name characters * @return true if string contains invalid characters, false otherwise * * This function checks if the input string contains any of the following invalid characters * for Active Directory account names: * & : ] [ + | ; $ * ? < > ! space / \ ' ` ~ * * These characters are defined in the invalid_characters_ad_name vector according to * Microsoft documentation: * https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb726984(v=technet.10) */ static bool contains_invalid_characters_in_ad_account_name( const std::string& value ) { std::vector<char> invalid_characters_ad_name = { '&', ':', ']', '[', '+', '|', ';', '$', '*', '?', '<', '>', '!', ' ', '/', '\\', '\'', '`', '~' }; bool result = false; // Iterate over all characters in invalid_path_characters vector for ( const char& ch : invalid_characters_ad_name ) { // Check if character exist in string if ( value.find( ch ) != std::string::npos ) { result = true; break; } } return result; } };