From b2673c5d08837084655230ccb097ac44df19788a Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Tue, 12 Jan 2016 11:05:02 +0100 Subject: [PATCH 2/2] 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 --- Makefile.am | 1 + src/config/SSSDConfig/__init__.py.in | 2 + src/config/etc/sssd.api.d/sssd-ad.conf | 2 + src/man/sssd-ad.5.xml | 33 +++ src/providers/ad/ad_common.h | 5 + src/providers/ad/ad_init.c | 7 + src/providers/ad/ad_machine_pw_renewal.c | 376 +++++++++++++++++++++++++++++++ src/providers/ad/ad_opts.c | 2 + src/util/util_errors.c | 1 + src/util/util_errors.h | 1 + 10 files changed, 430 insertions(+) create mode 100644 src/providers/ad/ad_machine_pw_renewal.c diff --git a/Makefile.am b/Makefile.am index a9d3f25d3775f6ac824b9f9b85dd0412417c33d3..8f72c4b440d0745becd1675c3cc23fdb003645bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3028,6 +3028,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 fe2971d993ccf2975bbddbe6c74572ab9f5d4e51..647d081255d738c028f73bc5d65eff58cebdbdf1 100644 --- a/src/config/SSSDConfig/__init__.py.in +++ b/src/config/SSSDConfig/__init__.py.in @@ -200,6 +200,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 5eb546caac913b839112a70bd81dbde2c7ff2d9f..0ea73d14112d1c7cf7a6d4cbda0d2b2e53a3a7be 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 725663b7d1e730fbec8cbabc1ad5526a7bb1aac5..05520d14d0b6d7b61210e6a67d7f1b88ee492ad0 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 49e97b0bebf18e795be2549c22de74111ae06583..a932a14d0362a961a83efa254419c5a32f353458 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 */ }; @@ -179,4 +181,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 dp_option *opts); + #endif /* AD_COMMON_H_ */ diff --git a/src/providers/ad/ad_init.c b/src/providers/ad/ad_init.c index 72ce5536b0f0f69a530bda0ffc41ae93180c1a94..2c6027b04aafc995188d006cc5b0bdb97f684b30 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->basic); + 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 0000000000000000000000000000000000000000..37fab3e953c219ba593ab1feb39ef80302b6e47a --- /dev/null +++ b/src/providers/ad/ad_machine_pw_renewal.c @@ -0,0 +1,376 @@ +/* + 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 *ad_domain; + char *ad_hostname; + char *ad_keytab; + size_t pw_lifetime_in_days; + size_t period; + size_t initial_delay; + char *prog_path; + const char **extra_args; +}; + +static errno_t get_adcli_extra_args(struct renewal_data *renewal_data) +{ + const char **args; + size_t c = 0; + + 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", + renewal_data->pw_lifetime_in_days); + args[c++] = talloc_asprintf(args, "--host-fqdn=%s", + renewal_data->ad_hostname); + args[c++] = talloc_asprintf(args, "--host-keytab=%s", + renewal_data->ad_keytab); + args[c++] = talloc_asprintf(args, "--domain=%s", renewal_data->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_period(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", + 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 dp_option *opts) +{ + int ret; + struct renewal_data *renewal_data; + int lifetime; + const char *dummy; + char **opt_list; + int opt_list_size; + char *endptr; + + lifetime = dp_opt_get_int(opts, 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(opts, 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; + renewal_data->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; + renewal_data->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; + } + + renewal_data->pw_lifetime_in_days = lifetime; + renewal_data->ad_domain = talloc_strdup(renewal_data, + dp_opt_get_string(opts, AD_DOMAIN)); + + renewal_data->ad_hostname = talloc_strdup(renewal_data, + dp_opt_get_string(opts, + AD_HOSTNAME)); + renewal_data->ad_keytab = talloc_strdup(renewal_data, + dp_opt_get_string(opts, AD_KEYTAB)); + if (renewal_data->ad_domain == NULL || renewal_data->ad_hostname == NULL + || renewal_data->ad_keytab == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing AD domain or hostname.\n"); + ret = EINVAL; + goto done; + } + + ret = get_adcli_extra_args(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, renewal_data->period, + renewal_data->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 4ea96637ca7264c76109ed8c2f7b5e8a94f73bfe..8b2841eadc0236b51f8c9c2c02b7c98837fbe416 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 ed19346d9b588a711367af4c891b1298cd4f067e..1d684d387b90b8db37609d5bc022e06fcac708f9 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 c1d081912a382d645c27809a3ac336ff90047cdf..5c02fdd8b4c6e0c59f7fd6f66a3fc8a8e48dc607 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 */ }; -- 2.1.0