From 20ed1a2063e0463c9e97870ea4e5e607467b041e Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Jan 19 2016 16:54:27 +0000 Subject: AD: add task to renew the machine account password if needed AD expects its clients to renew the machine account password on a regular basis, be default every 30 days. Even if a client does not renew the password it might not cause issues because AD does not enforce the renewal. But the password age might be used to identify unused machine accounts in large environments which might get disabled or deleted automatically. With this patch SSSD calls an external program to check the age of the machine account password and renew it if needed. Currently 'adcli' is used as external program which is able to renew the password since version 0.8.0. Resolves https://fedorahosted.org/sssd/ticket/1041 Reviewed-by: Jakub Hrozek (cherry picked from commit 5f7cd30c865046a7ea69944f7e07c85b4c43465a) --- diff --git a/Makefile.am b/Makefile.am index 1c0b1aa..a9099c0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3061,6 +3061,7 @@ libsss_ad_la_SOURCES = \ src/providers/ad/ad_common.h \ src/providers/ad/ad_init.c \ src/providers/ad/ad_dyndns.c \ + src/providers/ad/ad_machine_pw_renewal.c \ src/providers/ad/ad_id.c \ src/providers/ad/ad_id.h \ src/providers/ad/ad_access.c \ diff --git a/src/config/SSSDConfig/__init__.py.in b/src/config/SSSDConfig/__init__.py.in index 2cb8570..b4a6fcb 100644 --- a/src/config/SSSDConfig/__init__.py.in +++ b/src/config/SSSDConfig/__init__.py.in @@ -199,6 +199,8 @@ option_strings = { 'ad_gpo_map_deny' : _('PAM service names for which GPO-based access is always denied'), 'ad_gpo_default_right' : _('Default logon right (or permit/deny) to use for unmapped PAM service names'), 'ad_site' : _('a particular site to be used by the client'), + 'ad_maximum_machine_account_password_age' : _('Maximum age in days before the machine account password should be renewed'), + 'ad_machine_account_password_renewal_opts' : _('Option for tuing the machine account renewal task'), # [provider/krb5] 'krb5_kdcip' : _('Kerberos server address'), diff --git a/src/config/etc/sssd.api.d/sssd-ad.conf b/src/config/etc/sssd.api.d/sssd-ad.conf index 5eb546c..0ea73d1 100644 --- a/src/config/etc/sssd.api.d/sssd-ad.conf +++ b/src/config/etc/sssd.api.d/sssd-ad.conf @@ -17,6 +17,8 @@ ad_gpo_map_permit = str, None, false ad_gpo_map_deny = str, None, false ad_gpo_default_right = str, None, false ad_site = str, None, false +ad_maximum_machine_account_password_age = int, None, false +ad_machine_account_password_renewal_opts = str, None, false ldap_uri = str, None, false ldap_backup_uri = str, None, false ldap_search_base = str, None, false diff --git a/src/man/sssd-ad.5.xml b/src/man/sssd-ad.5.xml index 173fb93..4280eac 100644 --- a/src/man/sssd-ad.5.xml +++ b/src/man/sssd-ad.5.xml @@ -719,6 +719,39 @@ ad_gpo_map_deny = +my_pam_service + ad_maximum_machine_account_password_age (integer) + + + SSSD will check once a day if the machine account + password is older than the given age in days and try + to renew it. A value of 0 will disable the renewal + attempt. + + + Default: 30 days + + + + + + ad_machine_account_password_renewal_opts (string) + + + This option should only be used to test the machine + account renewal task. The option expect 2 integers + seperated by a colon (':'). The first integer + defines the interval in seconds how often the task + is run. The second specifies the inital timeout in + seconds before the task is run for the first time + after startup. + + + Default: 86400:750 (24h and 15m) + + + + + dyndns_update (boolean) diff --git a/src/providers/ad/ad_common.h b/src/providers/ad/ad_common.h index 2dd4175..5bb2e52 100644 --- a/src/providers/ad/ad_common.h +++ b/src/providers/ad/ad_common.h @@ -62,6 +62,8 @@ enum ad_basic_opt { AD_GPO_DEFAULT_RIGHT, AD_SITE, AD_KRB5_CONFD_PATH, + AD_MAXIMUM_MACHINE_ACCOUNT_PASSWORD_AGE, + AD_MACHINE_ACCOUNT_PASSWORD_RENEWAL_OPTS, AD_OPTS_BASIC /* opts counter */ }; @@ -180,4 +182,7 @@ int ad_autofs_init(struct be_ctx *be_ctx, struct bet_ops **ops, void **pvt_data); +errno_t ad_machine_account_password_renewal_init(struct be_ctx *be_ctx, + struct ad_options *ad_opts); + #endif /* AD_COMMON_H_ */ diff --git a/src/providers/ad/ad_init.c b/src/providers/ad/ad_init.c index 72ce553..e40fb6f 100644 --- a/src/providers/ad/ad_init.c +++ b/src/providers/ad/ad_init.c @@ -308,6 +308,13 @@ sssm_ad_id_init(struct be_ctx *bectx, "will not work [%d]: %s\n", ret, strerror(ret)); } + ret = ad_machine_account_password_renewal_init(bectx, ad_options); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot setup task for machine account " + "password renewal.\n"); + goto done; + } + *ops = &ad_id_ops; *pvt_data = ad_ctx; diff --git a/src/providers/ad/ad_machine_pw_renewal.c b/src/providers/ad/ad_machine_pw_renewal.c new file mode 100644 index 0000000..e42c700 --- /dev/null +++ b/src/providers/ad/ad_machine_pw_renewal.c @@ -0,0 +1,372 @@ +/* + SSSD + + Authors: + Sumit Bose + + Copyright (C) 2016 Red Hat + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#include "util/util.h" +#include "util/strtonum.h" +#include "providers/dp_ptask.h" +#include "providers/ad/ad_common.h" + +#ifndef RENEWAL_PROG_PATH +#define RENEWAL_PROG_PATH "/usr/sbin/adcli" +#endif + +struct renewal_data { + char *prog_path; + const char **extra_args; +}; + +static errno_t get_adcli_extra_args(const char *ad_domain, + const char *ad_hostname, + const char *ad_keytab, + size_t pw_lifetime_in_days, + size_t period, + size_t initial_delay, + struct renewal_data *renewal_data) +{ + const char **args; + size_t c = 0; + + if (ad_domain == NULL || ad_hostname == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing AD domain or hostname.\n"); + return EINVAL; + } + + renewal_data->prog_path = talloc_strdup(renewal_data, RENEWAL_PROG_PATH); + if (renewal_data->prog_path == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n"); + return ENOMEM; + } + + args = talloc_array(renewal_data, const char *, 7); + if (args == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc_array failed.\n"); + return ENOMEM; + } + + /* extra_args are added in revers order */ + args[c++] = talloc_asprintf(args, "--computer-password-lifetime=%zu", + pw_lifetime_in_days); + args[c++] = talloc_asprintf(args, "--host-fqdn=%s", ad_hostname); + if (ad_keytab != NULL) { + args[c++] = talloc_asprintf(args, "--host-keytab=%s", ad_keytab); + } + args[c++] = talloc_asprintf(args, "--domain=%s", ad_domain); + if (DEBUG_IS_SET(SSSDBG_TRACE_LIBS)) { + args[c++] = talloc_strdup(args, "--verbose"); + } + args[c++] = talloc_strdup(args, "update"); + args[c] = NULL; + + do { + if (args[--c] == NULL) { + DEBUG(SSSDBG_OP_FAILURE, + "talloc failed while copying arguments.\n"); + talloc_free(args); + return ENOMEM; + } + } while (c != 0); + + renewal_data->extra_args = args; + + return EOK; +} + +struct renewal_state { + int child_status; + struct sss_child_ctx_old *child_ctx; + struct tevent_timer *timeout_handler; + struct tevent_context *ev; + + int write_to_child_fd; + int read_from_child_fd; +}; + +static void ad_machine_account_password_renewal_done(struct tevent_req *subreq); +static void +ad_machine_account_password_renewal_timeout(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, void *pvt); + +static struct tevent_req * +ad_machine_account_password_renewal_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct be_ctx *be_ctx, + struct be_ptask *be_ptask, + void *pvt) +{ + struct renewal_data *renewal_data; + struct renewal_state *state; + struct tevent_req *req; + struct tevent_req *subreq; + pid_t child_pid; + struct timeval tv; + int pipefd_to_child[2]; + int pipefd_from_child[2]; + int ret; + + req = tevent_req_create(mem_ctx, &state, struct renewal_state); + if (req == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "tevent_req_create failed.\n"); + return NULL; + } + + renewal_data = talloc_get_type(pvt, struct renewal_data); + + state->ev = ev; + state->child_status = EFAULT; + state->read_from_child_fd = -1; + state->write_to_child_fd = -1; + + ret = pipe(pipefd_from_child); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "pipe failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + ret = pipe(pipefd_to_child); + if (ret == -1) { + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, + "pipe failed [%d][%s].\n", ret, strerror(ret)); + goto done; + } + + child_pid = fork(); + if (child_pid == 0) { /* child */ + ret = exec_child_ex(state, pipefd_to_child, pipefd_from_child, + renewal_data->prog_path, -1, + renewal_data->extra_args, true, + STDIN_FILENO, STDERR_FILENO); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Could not exec renewal child: [%d][%s].\n", + ret, strerror(ret)); + goto done; + } + } else if (child_pid > 0) { /* parent */ + + state->read_from_child_fd = pipefd_from_child[0]; + close(pipefd_from_child[1]); + sss_fd_nonblocking(state->read_from_child_fd); + + state->write_to_child_fd = pipefd_to_child[1]; + close(pipefd_to_child[0]); + sss_fd_nonblocking(state->write_to_child_fd); + + /* Set up SIGCHLD handler */ + ret = child_handler_setup(ev, child_pid, NULL, NULL, &state->child_ctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "Could not set up child handlers [%d]: %s\n", + ret, sss_strerror(ret)); + ret = ERR_RENEWAL_CHILD; + goto done; + } + + /* Set up timeout handler */ + tv = tevent_timeval_current_ofs(be_ptask_get_timeout(be_ptask), 0); + state->timeout_handler = tevent_add_timer(ev, req, tv, + ad_machine_account_password_renewal_timeout, + req); + if(state->timeout_handler == NULL) { + ret = ERR_RENEWAL_CHILD; + goto done; + } + + subreq = read_pipe_send(state, ev, state->read_from_child_fd); + if (subreq == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "read_pipe_send failed.\n"); + ret = ERR_RENEWAL_CHILD; + goto done; + } + tevent_req_set_callback(subreq, + ad_machine_account_password_renewal_done, req); + + /* Now either wait for the timeout to fire or the child + * to finish + */ + } else { /* error */ + ret = errno; + DEBUG(SSSDBG_CRIT_FAILURE, "fork failed [%d][%s].\n", + ret, sss_strerror(ret)); + goto done; + } + + ret = EOK; + +done: + if (ret != EOK) { + tevent_req_error(req, ret); + tevent_req_post(req, ev); + } + return req; +} + +static void ad_machine_account_password_renewal_done(struct tevent_req *subreq) +{ + uint8_t *buf; + ssize_t buf_len; + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct renewal_state *state = tevent_req_data(req, struct renewal_state); + int ret; + + talloc_zfree(state->timeout_handler); + + ret = read_pipe_recv(subreq, state, &buf, &buf_len); + talloc_zfree(subreq); + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + DEBUG(SSSDBG_TRACE_LIBS, "--- adcli output start---\n" + "%.*s" + "---adcli output end---\n", + (int) buf_len, buf); + + close(state->read_from_child_fd); + state->read_from_child_fd = -1; + + + tevent_req_done(req); + return; +} + +static void +ad_machine_account_password_renewal_timeout(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval tv, void *pvt) +{ + struct tevent_req *req = talloc_get_type(pvt, struct tevent_req); + struct renewal_state *state = tevent_req_data(req, struct renewal_state); + + DEBUG(SSSDBG_CRIT_FAILURE, "Timeout reached for AD renewal child.\n"); + child_handler_destroy(state->child_ctx); + state->child_ctx = NULL; + state->child_status = ETIMEDOUT; + tevent_req_error(req, ERR_RENEWAL_CHILD); +} + +static errno_t +ad_machine_account_password_renewal_recv(struct tevent_req *req) +{ + + TEVENT_REQ_RETURN_ON_ERROR(req); + + return EOK; +} + +errno_t ad_machine_account_password_renewal_init(struct be_ctx *be_ctx, + struct ad_options *ad_opts) +{ + int ret; + struct renewal_data *renewal_data; + int lifetime; + size_t period; + size_t initial_delay; + const char *dummy; + char **opt_list; + int opt_list_size; + char *endptr; + + lifetime = dp_opt_get_int(ad_opts->basic, + AD_MAXIMUM_MACHINE_ACCOUNT_PASSWORD_AGE); + + if (lifetime == 0) { + DEBUG(SSSDBG_CONF_SETTINGS, "Automatic machine account renewal disabled.\n"); + return EOK; + } + + if (lifetime < 0) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Illegal value [%d] for password lifetime.\n", lifetime); + return EINVAL; + } + + renewal_data = talloc(be_ctx, struct renewal_data); + if (renewal_data == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "talloc failed.\n"); + return ENOMEM; + } + + dummy = dp_opt_get_cstring(ad_opts->basic, + AD_MACHINE_ACCOUNT_PASSWORD_RENEWAL_OPTS); + ret = split_on_separator(renewal_data, dummy, ':', true, false, + &opt_list, &opt_list_size); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "split_on_separator failed.\n"); + goto done; + } + + if (opt_list_size != 2) { + DEBUG(SSSDBG_CRIT_FAILURE, "Wrong number of renewal options.\n"); + ret = EINVAL; + goto done; + } + + errno = 0; + period = strtouint32(opt_list[0], &endptr, 10); + if (errno != 0 || *endptr != '\0' || opt_list[0] == endptr) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to parse first renewal option.\n"); + ret = EINVAL; + goto done; + } + + errno = 0; + initial_delay = strtouint32(opt_list[1], &endptr, 10); + if (errno != 0 || *endptr != '\0' || opt_list[0] == endptr) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unable to parse second renewal option.\n"); + ret = EINVAL; + goto done; + } + + ret = get_adcli_extra_args(dp_opt_get_cstring(ad_opts->basic, AD_DOMAIN), + dp_opt_get_cstring(ad_opts->basic, AD_HOSTNAME), + dp_opt_get_cstring(ad_opts->id_ctx->sdap_id_ctx->opts->basic, + SDAP_KRB5_KEYTAB), + lifetime, period, initial_delay, renewal_data); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "get_adcli_extra_args failed.\n"); + goto done; + } + + ret = be_ptask_create(be_ctx, be_ctx, period, initial_delay, 0, 0, 60, + BE_PTASK_OFFLINE_DISABLE, 0, + ad_machine_account_password_renewal_send, + ad_machine_account_password_renewal_recv, + renewal_data, + "AD machine account password renewal", NULL); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, "be_ptask_create failed.\n"); + goto done; + } + + ret = EOK; + +done: + if (ret != EOK) { + talloc_free(renewal_data); + } + + return ret; +} diff --git a/src/providers/ad/ad_opts.c b/src/providers/ad/ad_opts.c index 4ea9663..8b2841e 100644 --- a/src/providers/ad/ad_opts.c +++ b/src/providers/ad/ad_opts.c @@ -48,6 +48,8 @@ struct dp_option ad_basic_opts[] = { { "ad_gpo_default_right", DP_OPT_STRING, NULL_STRING, NULL_STRING }, { "ad_site", DP_OPT_STRING, NULL_STRING, NULL_STRING }, { "krb5_confd_path", DP_OPT_STRING, { KRB5_MAPPING_DIR }, NULL_STRING }, + { "ad_maximum_machine_account_password_age", DP_OPT_NUMBER, { .number = 30 }, NULL_NUMBER }, + { "ad_machine_account_password_renewal_opts", DP_OPT_STRING, { "86400:750" }, NULL_STRING }, DP_OPTION_TERMINATOR }; diff --git a/src/util/util_errors.c b/src/util/util_errors.c index ed19346..1d684d3 100644 --- a/src/util/util_errors.c +++ b/src/util/util_errors.c @@ -82,6 +82,7 @@ struct err_string error_to_str[] = { { "Address family not supported" }, /* ERR_ADDR_FAMILY_NOT_SUPPORTED */ { "Message sender is the bus" }, /* ERR_SBUS_SENDER_BUS */ { "Subdomain is inactive" }, /* ERR_SUBDOM_INACTIVE */ + { "AD renewal child failed" }, /* ERR_RENEWAL_CHILD */ { "ERR_LAST" } /* ERR_LAST */ }; diff --git a/src/util/util_errors.h b/src/util/util_errors.h index c1d0819..5c02fdd 100644 --- a/src/util/util_errors.h +++ b/src/util/util_errors.h @@ -104,6 +104,7 @@ enum sssd_errors { ERR_ADDR_FAMILY_NOT_SUPPORTED, ERR_SBUS_SENDER_BUS, ERR_SUBDOM_INACTIVE, + ERR_RENEWAL_CHILD, ERR_LAST /* ALWAYS LAST */ };