From 41379f7ad6a9442dd55cc43d832427911e86db31 Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Fri, 23 Oct 2020 16:53:43 +0200 Subject: [PATCH 2/7] computer: add create-msa sub-command Add new sub-command to create a managed service account in AD. This can be used if LDAP access to AD is needed but the host is already joined to a different domain. Resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1854112 --- doc/adcli.xml | 140 ++++++++++++++++++++++++++++++++++++++ library/adenroll.c | 164 ++++++++++++++++++++++++++++++++++++++------- tools/computer.c | 125 ++++++++++++++++++++++++++++++++++ tools/tools.c | 1 + tools/tools.h | 4 ++ 5 files changed, 409 insertions(+), 25 deletions(-) diff --git a/doc/adcli.xml b/doc/adcli.xml index cc44fd8..14921f9 100644 --- a/doc/adcli.xml +++ b/doc/adcli.xml @@ -98,6 +98,10 @@ --domain=domain.example.com computer + + adcli create-msa + --domain=domain.example.com + @@ -885,6 +889,142 @@ Password for Administrator: + + Create a managed service account + + adcli create-msa creates a managed service + account (MSA) in the given Active Directory domain. This is useful if a + computer should not fully join the Active Directory domain but LDAP + access is needed. A typical use case is that the computer is already + joined an Active Directory domain and needs access to another Active + Directory domain in the same or a trusted forest where the host + credentials from the joined Active Directory domain are + not valid, e.g. there is only a one-way trust. + + +$ adcli create-msa --domain=domain.example.com +Password for Administrator: + + + The managed service account, as maintained by adcli, cannot have + additional service principals names (SPNs) associated with it. An SPN + is defined within the context of a Kerberos service which is tied to a + machine account in Active Directory. Since a machine can be joined to a + single Active Directory domain, managed service account in a different + Active Directory domain will not have the SPNs that otherwise are part + of another Active Directory domain's machine. + + Since it is expected that a client will most probably join to the + Active Directory domain matching its DNS domain the managed service + account will be needed for a different Active directory domain and as a + result the Active Directory domain name is a mandatory option. If + called with no other options adcli create-msa + will use the short hostname with an additional random suffix as + computer name to avoid name collisions. + + LDAP attribute sAMAccountName has a limit of 20 characters. + However, machine account's NetBIOS name must be at most 16 characters + long, including a trailing '$' sign. Since it is not expected that the + managed service accounts created by adcli will be used on the NetBIOS + level the remaining 4 characters can be used to add uniqueness. Managed + service account names will have a suffix of 3 random characters from + number and upper- and lowercase ASCII ranges appended to the chosen + short host name, using '!' as a separator. For a host with the + shortname 'myhost', a managed service account will have a common name + (CN attribute) 'myhost!A2c' and a NetBIOS name + (sAMAccountName attribute) will be 'myhost!A2c$'. A corresponding + Kerberos principal in the Active Directory domain where the managed + service account was created would be + 'myhost!A2c$@DOMAIN.EXAMPLE.COM'. + + A keytab for the managed service account is stored into a file + specified with -K option. If it is not specified, the file is named + after the default keytab file, with lowercase Active Directory domain + of the managed service account as a suffix. On most systems it would be + /etc/krb5.keytab with a suffix of + 'domain.example.com', e.g. + /etc/krb5.keytad.domain.example.com. + + adcli create-msa can be called multiple + times to reset the password of the managed service account. To identify + the right account with the random component in the name the + corresponding principal is read from the keytab. If the keytab got + deleted adcli will try to identify an existing + managed service account with the help of the fully-qualified name, if + this fails a new managed service account will be created. + + The managed service account password can be updated with + +$ adcli update --domain=domain.example.com --host-keytab=/etc/krb5.keytad.domain.example.com + + and the managed service account can be deleted with + +$ adcli delete-computer --domain=domain.example.com 'myhost!A2c' + + + + In addition to the global options, you can specify the following + options to control how this operation is done. + + + + + The short non-dotted name of the managed + service account that will be created in the Active + Directory domain. The long option name + is + kept to underline the similarity with the same option + of the other sub-commands. If not specified, + then the first portion of the + or its default is used with a random suffix. + + + + The full distinguished name of the OU in + which to create the managed service account. If not + specified, then the managed service account will be + created in a default location. + + + + Override the local machine's fully + qualified DNS domain name. If not specified, the local + machine's hostname will be retrieved via + gethostname(). + If gethostname() only returns a short name + getaddrinfo() with the AI_CANONNAME hint + is called to expand the name to a fully qualified DNS + domain name. + + + + Specify the path to the host keytab where + credentials of the managed service account will be + written after a successful creation. If not specified, + the default location will be used, usually + /etc/krb5.keytab with + the lower-cased Active Directory domain name added as a + suffix e.g. + /etc/krb5.keytab.domain.example.com. + + + + + After a successful creation print out + information about the created object. This is output in + a format that should be both human and machine + readable. + + + + After a successful creation print out + the managed service account password. This is output in + a format that should be both human and machine + readable. + + + + Delegated Permissions It is common practice in AD to not use an account from the Domain diff --git a/library/adenroll.c b/library/adenroll.c index 5ae1f7b..dbfda36 100644 --- a/library/adenroll.c +++ b/library/adenroll.c @@ -155,6 +155,20 @@ struct _adcli_enroll { char *description; }; +static void +check_if_service (adcli_enroll *enroll, + LDAP *ldap, + LDAPMessage *results) +{ + char **objectclasses = NULL; + + objectclasses = _adcli_ldap_parse_values (ldap, results, "objectClass"); + enroll->is_service = _adcli_strv_has_ex (objectclasses, + "msDS-ManagedServiceAccount", + strcasecmp) == 1 ? true : false; + _adcli_strv_free (objectclasses); +} + static adcli_result ensure_host_fqdn (adcli_result res, adcli_enroll *enroll) @@ -471,13 +485,15 @@ ensure_keytab_principals (adcli_result res, { krb5_context k5; krb5_error_code code; - int count; + int count = 0; int at, i; /* Prepare the principals we're going to add to the keytab */ - return_unexpected_if_fail (enroll->service_principals); - count = _adcli_strv_len (enroll->service_principals); + if (!enroll->is_service) { + return_unexpected_if_fail (enroll->service_principals); + count = _adcli_strv_len (enroll->service_principals); + } k5 = adcli_conn_get_krb5_context (enroll->conn); return_unexpected_if_fail (k5 != NULL); @@ -556,8 +572,12 @@ static adcli_result lookup_computer_container (adcli_enroll *enroll, LDAP *ldap) { - char *attrs[] = { "wellKnownObjects", NULL }; - char *prefix = "B:32:AA312825768811D1ADED00C04FD8D5CD:"; + char *attrs[] = { enroll->is_service ? "otherWellKnownObjects" + : "wellKnownObjects", NULL }; + const char *prefix = enroll->is_service ? "B:32:1EB93889E40C45DF9F0C64D23BBB6237:" + : "B:32:AA312825768811D1ADED00C04FD8D5CD:"; + const char *filter = enroll->is_service ? "(&(objectClass=container)(cn=Managed Service Accounts))" + : "(&(objectClass=container)(cn=Computers))"; int prefix_len; LDAPMessage *results; const char *base; @@ -586,7 +606,7 @@ lookup_computer_container (adcli_enroll *enroll, "Couldn't lookup computer container: %s", base); } - values = _adcli_ldap_parse_values (ldap, results, "wellKnownObjects"); + values = _adcli_ldap_parse_values (ldap, results, attrs[0]); ldap_msgfree (results); prefix_len = strlen (prefix); @@ -604,8 +624,7 @@ lookup_computer_container (adcli_enroll *enroll, /* Try harder */ if (!enroll->computer_container) { - ret = ldap_search_ext_s (ldap, base, LDAP_SCOPE_BASE, - "(&(objectClass=container)(cn=Computers))", + ret = ldap_search_ext_s (ldap, base, LDAP_SCOPE_BASE, filter, attrs, 0, NULL, NULL, NULL, -1, &results); if (ret == LDAP_SUCCESS) { enroll->computer_container = _adcli_ldap_parse_dn (ldap, results); @@ -747,7 +766,7 @@ static adcli_result create_computer_account (adcli_enroll *enroll, LDAP *ldap) { - char *vals_objectClass[] = { "computer", NULL }; + char *vals_objectClass[] = { enroll->is_service ? "msDS-ManagedServiceAccount" : "computer", NULL }; LDAPMod objectClass = { LDAP_MOD_ADD, "objectClass", { vals_objectClass, } }; char *vals_sAMAccountName[] = { enroll->computer_sam, NULL }; LDAPMod sAMAccountName = { LDAP_MOD_ADD, "sAMAccountName", { vals_sAMAccountName, } }; @@ -806,7 +825,7 @@ create_computer_account (adcli_enroll *enroll, m = 0; for (c = 0; c < mods_count - 1; c++) { /* Skip empty LDAP sttributes */ - if (all_mods[c]->mod_vals.modv_strvals[0] != NULL) { + if (all_mods[c]->mod_vals.modv_strvals != NULL && all_mods[c]->mod_vals.modv_strvals[0] != NULL) { mods[m++] = all_mods[c]; } } @@ -936,7 +955,7 @@ locate_computer_account (adcli_enroll *enroll, LDAPMessage **rresults, LDAPMessage **rentry) { - char *attrs[] = { "1.1", NULL }; + char *attrs[] = { "objectClass", NULL }; LDAPMessage *results = NULL; LDAPMessage *entry = NULL; const char *base; @@ -948,7 +967,9 @@ locate_computer_account (adcli_enroll *enroll, /* If we don't yet know our computer dn, then try and find it */ value = _adcli_ldap_escape_filter (enroll->computer_sam); return_unexpected_if_fail (value != NULL); - if (asprintf (&filter, "(&(objectClass=computer)(sAMAccountName=%s))", value) < 0) + if (asprintf (&filter, "(&(objectClass=%s)(sAMAccountName=%s))", + enroll->is_service ? "msDS-ManagedServiceAccount" : "computer", + value) < 0) return_unexpected_if_reached (); free (value); @@ -962,8 +983,11 @@ locate_computer_account (adcli_enroll *enroll, if (ret == LDAP_SUCCESS) { entry = ldap_first_entry (ldap, results); - /* If we found a computer account, make note of dn */ + /* If we found a computer/service account, make note of dn */ if (entry) { + if (!enroll->is_service_explicit) { + check_if_service ( enroll, ldap, results); + } dn = ldap_get_dn (ldap, entry); free (enroll->computer_dn); enroll->computer_dn = strdup (dn); @@ -1003,7 +1027,7 @@ load_computer_account (adcli_enroll *enroll, LDAPMessage **rresults, LDAPMessage **rentry) { - char *attrs[] = { "1.1", NULL }; + char *attrs[] = { "objectClass", NULL }; LDAPMessage *results = NULL; LDAPMessage *entry = NULL; int ret; @@ -1081,6 +1105,12 @@ locate_or_create_computer_account (adcli_enroll *enroll, if (res == ADCLI_SUCCESS && entry == NULL) res = create_computer_account (enroll, ldap); + /* Service account already exists, just continue and update the + * password */ + if (enroll->is_service && entry != NULL) { + res = ADCLI_SUCCESS; + } + if (results) ldap_msgfree (results); @@ -1413,6 +1443,11 @@ update_computer_account (adcli_enroll *enroll) LDAP *ldap; char *value = NULL; + /* No updates for service accounts */ + if (enroll->is_service) { + return; + } + ldap = adcli_conn_get_ldap_connection (enroll->conn); return_if_fail (ldap != NULL); @@ -1501,6 +1536,11 @@ update_service_principals (adcli_enroll *enroll) LDAP *ldap; int ret; + /* No updates for service accounts */ + if (enroll->is_service) { + return ADCLI_SUCCESS; + } + ldap = adcli_conn_get_ldap_connection (enroll->conn); return_unexpected_if_fail (ldap != NULL); @@ -1614,6 +1654,8 @@ load_keytab_entry (krb5_context k5, enroll->computer_name = name; name[len - 1] = '\0'; _adcli_info ("Found computer name in keytab: %s", name); + adcli_conn_set_computer_name (enroll->conn, + enroll->computer_name); name = NULL; } else if (!enroll->host_fqdn && _adcli_str_has_prefix (name, "host/") && strchr (name, '.')) { @@ -2002,17 +2044,25 @@ adcli_enroll_prepare (adcli_enroll *enroll, adcli_clear_last_error (); - /* Basic discovery and figuring out enroll params */ - res = ensure_host_fqdn (res, enroll); - res = ensure_computer_name (res, enroll); - res = ensure_computer_sam (res, enroll); - res = ensure_user_principal (res, enroll); - res = ensure_computer_password (res, enroll); - if (!(flags & ADCLI_ENROLL_NO_KEYTAB)) + if (enroll->is_service) { + /* Ensure basic params for service accounts */ + res = ensure_computer_sam (res, enroll); + res = ensure_computer_password (res, enroll); res = ensure_host_keytab (res, enroll); - res = ensure_service_names (res, enroll); - res = ensure_service_principals (res, enroll); - res = ensure_keytab_principals (res, enroll); + res = ensure_keytab_principals (res, enroll); + } else { + /* Basic discovery and figuring out enroll params */ + res = ensure_host_fqdn (res, enroll); + res = ensure_computer_name (res, enroll); + res = ensure_computer_sam (res, enroll); + res = ensure_user_principal (res, enroll); + res = ensure_computer_password (res, enroll); + if (!(flags & ADCLI_ENROLL_NO_KEYTAB)) + res = ensure_host_keytab (res, enroll); + res = ensure_service_names (res, enroll); + res = ensure_service_principals (res, enroll); + res = ensure_keytab_principals (res, enroll); + } return res; } @@ -2157,6 +2207,58 @@ enroll_join_or_update_tasks (adcli_enroll *enroll, return update_keytab_for_principals (enroll, flags); } +static adcli_result +adcli_enroll_add_description_for_service_account (adcli_enroll *enroll) +{ + const char *fqdn; + char *desc; + + fqdn = adcli_conn_get_host_fqdn (enroll->conn); + return_unexpected_if_fail (fqdn != NULL); + if (asprintf (&desc, "Please do not edit, Service account for %s, " + "managed by adcli.", fqdn) < 0) { + return_unexpected_if_reached (); + } + + adcli_enroll_set_description (enroll, desc); + free (desc); + + return ADCLI_SUCCESS; +} + +static adcli_result +adcli_enroll_add_keytab_for_service_account (adcli_enroll *enroll) +{ + krb5_context k5; + krb5_error_code code; + char def_keytab_name[MAX_KEYTAB_NAME_LEN]; + char *lc_dom_name; + int ret; + + if (adcli_enroll_get_keytab_name (enroll) == NULL) { + k5 = adcli_conn_get_krb5_context (enroll->conn); + return_unexpected_if_fail (k5 != NULL); + + code = krb5_kt_default_name (k5, def_keytab_name, + sizeof (def_keytab_name)); + return_unexpected_if_fail (code == 0); + + lc_dom_name = strdup (adcli_conn_get_domain_name (enroll->conn)); + return_unexpected_if_fail (lc_dom_name != NULL); + _adcli_str_down (lc_dom_name); + + + ret = asprintf (&enroll->keytab_name, "%s.%s", def_keytab_name, + lc_dom_name); + free (lc_dom_name); + return_unexpected_if_fail (ret > 0); + } + + _adcli_info ("Using service account keytab: %s", enroll->keytab_name); + + return ADCLI_SUCCESS; +} + adcli_result adcli_enroll_join (adcli_enroll *enroll, adcli_enroll_flags flags) @@ -2172,7 +2274,14 @@ adcli_enroll_join (adcli_enroll *enroll, if (res != ADCLI_SUCCESS) return res; - res = ensure_default_service_names (enroll); + if (enroll->is_service) { + res = adcli_enroll_add_description_for_service_account (enroll); + if (res == ADCLI_SUCCESS) { + res = adcli_enroll_add_keytab_for_service_account (enroll); + } + } else { + res = ensure_default_service_names (enroll); + } if (res != ADCLI_SUCCESS) return res; @@ -2281,6 +2390,11 @@ adcli_enroll_update (adcli_enroll *enroll, } free (value); + /* We only support password changes for service accounts */ + if (enroll->is_service && (flags & ADCLI_ENROLL_PASSWORD_VALID)) { + return ADCLI_SUCCESS; + } + return enroll_join_or_update_tasks (enroll, flags); } diff --git a/tools/computer.c b/tools/computer.c index 5a97d8b..63fd374 100644 --- a/tools/computer.c +++ b/tools/computer.c @@ -1074,3 +1074,128 @@ adcli_tool_computer_show (adcli_conn *conn, adcli_enroll_unref (enroll); return 0; } + +int +adcli_tool_computer_managed_service_account (adcli_conn *conn, + int argc, + char *argv[]) +{ + adcli_enroll *enroll; + adcli_result res; + int show_password = 0; + int details = 0; + int opt; + + struct option options[] = { + { "domain", required_argument, NULL, opt_domain }, + { "domain-realm", required_argument, NULL, opt_domain_realm }, + { "domain-controller", required_argument, NULL, opt_domain_controller }, + { "use-ldaps", no_argument, 0, opt_use_ldaps }, + { "login-user", required_argument, NULL, opt_login_user }, + { "login-ccache", optional_argument, NULL, opt_login_ccache }, + { "host-fqdn", required_argument, 0, opt_host_fqdn }, + { "computer-name", required_argument, 0, opt_computer_name }, + { "host-keytab", required_argument, 0, opt_host_keytab }, + { "no-password", no_argument, 0, opt_no_password }, + { "stdin-password", no_argument, 0, opt_stdin_password }, + { "prompt-password", no_argument, 0, opt_prompt_password }, + { "domain-ou", required_argument, NULL, opt_domain_ou }, + { "show-details", no_argument, NULL, opt_show_details }, + { "show-password", no_argument, NULL, opt_show_password }, + { "verbose", no_argument, NULL, opt_verbose }, + { "help", no_argument, NULL, 'h' }, + { 0 }, + }; + + static adcli_tool_desc usages[] = { + { 0, "usage: adcli create-msa --domain=xxxx" }, + { 0 }, + }; + + enroll = adcli_enroll_new (conn); + if (enroll == NULL) { + warnx ("unexpected memory problems"); + return -1; + } + + while ((opt = adcli_tool_getopt (argc, argv, options)) != -1) { + switch (opt) { + case opt_one_time_password: + adcli_conn_set_allowed_login_types (conn, ADCLI_LOGIN_COMPUTER_ACCOUNT); + adcli_conn_set_computer_password (conn, optarg); + break; + case opt_show_details: + details = 1; + break; + case opt_show_password: + show_password = 1; + break; + case 'h': + case '?': + case ':': + adcli_tool_usage (options, usages); + adcli_tool_usage (options, common_usages); + adcli_enroll_unref (enroll); + return opt == 'h' ? 0 : 2; + default: + res = parse_option ((Option)opt, optarg, conn, enroll); + if (res != ADCLI_SUCCESS) { + adcli_enroll_unref (enroll); + return res; + } + break; + } + } + + argc -= optind; + argv += optind; + + if (argc == 1) + adcli_conn_set_domain_name (conn, argv[0]); + else if (argc > 1) { + warnx ("extra arguments specified"); + adcli_enroll_unref (enroll); + return 2; + } + + if (adcli_conn_get_domain_name (conn) == NULL) { + warnx ("domain name is required"); + adcli_enroll_unref (enroll); + return 2; + } + + adcli_enroll_set_is_service (enroll, true); + adcli_conn_set_allowed_login_types (conn, ADCLI_LOGIN_USER_ACCOUNT); + + res = adcli_enroll_load (enroll); + if (res != ADCLI_SUCCESS) { + /* ignored */ + } + + res = adcli_conn_connect (conn); + if (res != ADCLI_SUCCESS) { + warnx ("couldn't connect to %s domain: %s", + adcli_conn_get_domain_name (conn), + adcli_get_last_error ()); + adcli_enroll_unref (enroll); + return -res; + } + + res = adcli_enroll_join (enroll, 0); + if (res != ADCLI_SUCCESS) { + warnx ("Adding service account for %s failed: %s", + adcli_conn_get_domain_name (conn), + adcli_get_last_error ()); + adcli_enroll_unref (enroll); + return -res; + } + + if (details) + dump_details (conn, enroll, show_password); + else if (show_password) + dump_password (conn, enroll); + + adcli_enroll_unref (enroll); + + return 0; +} diff --git a/tools/tools.c b/tools/tools.c index 1b6d879..d0dcf98 100644 --- a/tools/tools.c +++ b/tools/tools.c @@ -60,6 +60,7 @@ struct { { "reset-computer", adcli_tool_computer_reset, "Reset a computer account", }, { "delete-computer", adcli_tool_computer_delete, "Delete a computer account", }, { "show-computer", adcli_tool_computer_show, "Show computer account attributes stored in AD", }, + { "create-msa", adcli_tool_computer_managed_service_account, "Create a managed service account in the given AD domain", }, { "create-user", adcli_tool_user_create, "Create a user account", }, { "delete-user", adcli_tool_user_delete, "Delete a user account", }, { "create-group", adcli_tool_group_create, "Create a group", }, diff --git a/tools/tools.h b/tools/tools.h index 3702875..82d5e4e 100644 --- a/tools/tools.h +++ b/tools/tools.h @@ -82,6 +82,10 @@ int adcli_tool_computer_show (adcli_conn *conn, int argc, char *argv[]); +int adcli_tool_computer_managed_service_account (adcli_conn *conn, + int argc, + char *argv[]); + int adcli_tool_user_create (adcli_conn *conn, int argc, char *argv[]); -- 2.28.0