]> eyrie.org Git - kerberos/kstart.git/commitdiff
Retry initial authentication until it succeeds
authorRuss Allbery <eagle@eyrie.org>
Fri, 25 Dec 2015 22:46:00 +0000 (14:46 -0800)
committerRuss Allbery <eagle@eyrie.org>
Fri, 25 Dec 2015 22:46:00 +0000 (14:46 -0800)
For both k5start with a command or -K and no -x flag, and krenew with
the -i flag, repeatedly retry the initial authentication.  The first
retry will be immediate, and then the commands will keep trying with
exponential backoff to one minute intervals, and then continuously at
one minute intervals until the command is killed or authentication
succeeds.  k5start and krenew will no longer start any other command
until the initial authentication succeeds, fixing startup behavior
when running a command that must have valid Kerberos tickets
immediately on start.  Based on a patch by Lars Hanke.

NEWS
framework.c
k5start.pod
krenew.pod
tests/data/command
tests/k5start/daemon-t
tests/krenew/basic-t

diff --git a/NEWS b/NEWS
index cdf876a8110d42652aba9a30d77389b54fb94c2b..c674b12b93fe2eaf76d613932b7d89d29ea005ec 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -8,9 +8,15 @@ kstart 4.2 (unreleased)
     run, attempting authentication every minute as if authentication had
     failed after it had started.  Patch from Rasmus Borup Hansen.
 
-    If -i is given to krenew and the initial ticket renewal failed, start
-    with the shorter wake-up interval of one minute just as if a
-    subsequent renewal failed.  Patch from Rasmus Borup Hansen.
+    For both k5start with a command or -K and no -x flag, and krenew with
+    the -i flag, repeatedly retry the initial authentication.  The first
+    retry will be immediate, and then the commands will keep trying with
+    exponential backoff to one minute intervals, and then continuously at
+    one minute intervals until the command is killed or authentication
+    succeeds.  k5start and krenew will no longer start any other command
+    until the initial authentication succeeds, fixing startup behavior
+    when running a command that must have valid Kerberos tickets
+    immediately on start.  Based on a patch by Lars Hanke.
 
     Clean up the temporary ticket cache on k5start failure if -o, -g, or
     -m were given.  Based on a patch by Rasmus Borup Hansen.
@@ -33,7 +39,7 @@ kstart 4.2 (unreleased)
     Andrew Deason.
 
     Fix k5start and krenew to not incorrectly reject the -b flag in
-    conjunction with -K or a command.  Patch from Dr. Lars Hanke.
+    conjunction with -K or a command.  Patch from Lars Hanke.
 
 kstart 4.1 (2012-01-07)
 
index 7d36704d153c618457e7634113245371d0c99db5..affc80395f8a5e8bc3aa2390b47fe98757f8bff1 100644 (file)
@@ -20,6 +20,7 @@
  * other is handled via callbacks.
  *
  * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2015 Russ Allbery <eagle@eyrie.org>
  * Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2014
  *     The Board of Trustees of the Leland Stanford Junior University
  *
@@ -211,6 +212,33 @@ done:
 }
 
 
+/*
+ * Retry the initial authentication when the program is first starting.  Retry
+ * the authentication immediately, then after one second, and keep trying with
+ * exponential backoff, maxing out at one minute and continuing until
+ * authentication succeeds or we exit due to signal.
+ */
+static krb5_error_code
+retry_auth(krb5_context ctx, struct config *config)
+{
+    krb5_error_code code;
+    struct timeval timeout;
+    unsigned int delay = 1;
+
+    code = config->auth(ctx, config, 0);
+    while (code != 0) {
+        timeout.tv_sec = delay;
+        timeout.tv_usec = 0;
+        delay = (delay < 30) ? delay * 2 : delay;
+        select(0, NULL, NULL, NULL, &timeout);
+        if (exit_signaled)
+            exit_cleanup(ctx, config, 1);
+        code = config->auth(ctx, config, 0);
+    }
+    return code;
+}
+
+
 /*
  * Write out a PID file given the path to the file and the PID to write.
  * Errors are reported but otherwise ignored.
@@ -298,7 +326,7 @@ run_framework(krb5_context ctx, struct config *config)
      * isn't expired.
      */
     if (config->happy_ticket == 0)
-        code = config->auth(ctx, config, code);
+        code = config->auth(ctx, config, 0);
     else {
         code = ticket_expired(ctx, config);
         if (code != 0)
@@ -310,7 +338,7 @@ run_framework(krb5_context ctx, struct config *config)
         exit_cleanup(ctx, config, status);
 
     /* If requested, run the aklog program. */
-    if (config->do_aklog)
+    if (code == 0 && config->do_aklog)
         command_run(aklog, config->verbose);
 
     /*
@@ -326,6 +354,26 @@ run_framework(krb5_context ctx, struct config *config)
     if (config->pidfile != NULL)
         write_pidfile(config->pidfile, getpid());
 
+    /*
+     * Now, if the initial authentication failed and we're ignoring initial
+     * failures, retry authentication until it succeeds so that we never start
+     * the command without authentication.  We don't set up signal handlers
+     * here, which means SIGHUP may terminate the program during this period
+     * but not after the command is started.  Any approach here is potentially
+     * inconsistent; that seems the simplest.
+     *
+     * Set up some signal handlers so that we remove the PID file if we exit
+     * via signal.  These will be overwritten by the command signal handlers
+     * if we start a command later.
+     */
+    if (code != 0 && config->ignore_errors) {
+        add_handler(ctx, config, exit_handler, SIGHUP, "SIGHUP");
+        add_handler(ctx, config, exit_handler, SIGTERM, "SIGTERM");
+        code = retry_auth(ctx, config);
+        if (code == 0 && config->do_aklog)
+            command_run(aklog, config->verbose);
+    }
+
     /* Spawn the external command, if we were told to run one. */
     if (config->command != NULL) {
         child = command_start(config->command[0], config->command);
index 6078b49ce38d889358a1ebbe5ab2be473421b501..704c526fd7e50e60f1f9c749883a64609848e378 100644 (file)
@@ -70,6 +70,13 @@ commands that should receive Ctrl-C.
 If a running B<k5start> receives an ALRM signal, it immediately refreshes
 the ticket cache regardless of whether it is in danger of expiring.
 
+If B<k5start> is run with a command or the B<-K> flag and the B<-x> flag
+is not given, it will keep trying even if the initial authentication
+fails.  It will retry the initial authentication immediately and then with
+exponential backoff to once per minute, and keep trying until
+authentication succeeds or it is killed.  The command, if any, will not be
+started until authentication succeeds.
+
 =head1 OPTIONS
 
 =over 4
@@ -209,10 +216,9 @@ default.
 If this option is not given but a command was given on the command line,
 the default interval is 60 minutes (1 hour).
 
-If an error occurs in the initial authentication or in refreshing the
-ticket cache, the wake-up interval will be shortened to one minute and
-the operation retried at that interval for as long as the error
-persists.
+If an error occurs in refreshing the ticket cache, the wake-up interval
+will be shortened to one minute and the operation retried at that interval
+for as long as the error persists.
 
 =item B<-k> I<ticket cache>
 
@@ -370,8 +376,12 @@ every 10 minutes to check if the ticket is about to expire.
         host/example.com
 
 Do the same, but using the default ticket cache and run the command
-/usr/local/bin/auth-backup.  B<k5start> will continue running until the
-command finishes.
+F</usr/local/bin/auth-backup>.  B<k5start> will continue running until the
+command finishes.  If the initial authentication fails, keep trying, and
+don't start the command until it succeeds.  This could be used during
+system startup for a command that must have valid tickets before starting,
+and tolerates having B<k5start> start before the network is completely set
+up.
 
     k5start -f /etc/krb5.keytab -K 10 -l 10h host/example.com \
         /usr/local/bin/auth-backup
index 84b4cc0dcbbeac643bce8adaf5c1d99416c85d4f..1465edabbf02a06cff8c3f75b3163dcd5094b660 100644 (file)
@@ -161,10 +161,10 @@ continue running, waking up to try again after the next check interval
 ticket cache and B<krenew> should stay around and act on that recreated
 ticket cache once it's present.
 
-Setup errors opening the Kerberos ticket cache or running the command will
-still cause B<krenew> to exit, even if this flag is given.  Only Kerberos
-errors and errors after B<krenew> is running and any command has been
-started will be ignored.
+If the initial ticket cache renew fails, B<krenew> will retry the renewal
+immediately and then with exponential backoff to once per minute, and keep
+trying until authentication succeeds or it is killed.  The command, if
+any, will not be started until cache renewal succeeds.
 
 This flag is only useful in daemon mode or when a command was given.
 
index 056c4a3a115d13de503d11b092f2db0e80553f94..6da3ab36c14a7e3e8e16bd1cfa4b38e4a99e8937 100755 (executable)
@@ -17,5 +17,17 @@ print "Starting\n";
 my $file = shift;
 open (OUT, '>', $file) or die "Cannot write to $file: $!\n";
 OUT->autoflush (1);
-print OUT "$$\n", getcwd, "\n", $ENV{KRB5CCNAME}, "\n";
+
+# Make sure that the ticket cache exists and is non-zero.  If so, report its
+# value to standard output; otherwise, report the empty string.  Either way,
+# report the current working directory.
+my $cache = '';
+if ($ENV{KRB5CCNAME}) {
+    $path = $ENV{KRB5CCNAME};
+    $path =~ s/^FILE://;
+    if (-s $path) {
+        $cache = $ENV{KRB5CCNAME};
+    }
+}
+print OUT "$$\n", getcwd, "\n", $cache, "\n";
 sleep 1000 while 1;
index 05a3061af240d92c1f169bf04ad81249a47e0fd4..a78e9ccbcb483deebd252bb625a4525aa6e61a6e 100755 (executable)
@@ -3,12 +3,14 @@
 # Tests for k5start daemon functionality.
 #
 # Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
 # Copyright 2008, 2009, 2011, 2012, 2014
 #     The Board of Trustees of the Leland Stanford Junior University
 #
 # See LICENSE for licensing terms.
 
 use Cwd;
+use File::Copy qw(copy);
 
 use Test::More;
 
@@ -30,7 +32,7 @@ require "$ENV{SOURCE}/libtest.pl";
 
 # Decide whether we have the configuration to run the tests.
 if (-f "$DATA/test.keytab" and -f "$DATA/test.principal") {
-    plan tests => 84;
+    plan tests => 86;
 } else {
     plan skip_all => "no keytab configuration";
     exit 0;
@@ -122,7 +124,7 @@ is ($out, '', ' and -q was added implicitly');
 $pid = contents ("$TMP/pid");
 ok (kill (0, $pid), ' and the PID file is correct');
 kill (15, $pid) or warn "Can't kill $pid: $!\n";
-select (undef, undef, undef, 0.2);
+select (undef, undef, undef, 2.0);
 ok (!-f "$TMP/pid", ' and the PID file was removed');
 unlink "$TMP/pid";
 
@@ -294,23 +296,29 @@ close OUT;
 ok (!-f "$TMP/pid", 'PID file cleaned up');
 ok (!-f "$TMP/child-pid", 'Child PID file cleaned up');
 
-# One more time to test SIGQUIT.
+# Finally, start a child program with an invalid keytab path.  This should
+# start and keep trying to authenticate but never start the actual child
+# process until the ticket cache does exist.  We also test SIGQUIT handling.
 unlink "$TMP/krb5cc_child", "$TMP/child-out";
 ($out, $err, $status)
     = command ($K5START, '-bK', 1, '-k', "$TMP/krb5cc_child", '-f',
-               "$DATA/test.keytab", '-p', "$TMP/pid", '-c', "$TMP/child-pid",
+               "$TMP/test.keytab", '-p', "$TMP/pid", '-c', "$TMP/child-pid",
                $principal, '--', "$ENV{SOURCE}/data/command",
                "$TMP/child-out");
 is ($status, 0, 'Backgrounding k5start works');
-is ($err, '', ' with no error output');
+like ($err, qr/^k5start: error getting credentials: /, ' with error output');
 is ($out, '', ' and output was redirected properly');
+$pid = contents ("$TMP/pid");
+ok (kill (0, $pid), 'k5start is running');
+select (undef, undef, undef, 1);
+ok (!-f "$TMP/child-pid", ' child did not start');
+ok (!-f "$TMP/child-out", ' and has no output');
+copy ("$DATA/test.keytab", "$TMP/test.keytab");
 $tries = 0;
 while (not -f "$TMP/child-pid" and $tries < 100) {
     select (undef, undef, undef, 0.1);
     $tries++;
 }
-$pid = contents ("$TMP/pid");
-ok (kill (0, $pid), 'k5start is running');
 $child = contents ("$TMP/child-pid");
 ok (kill (0, $child), 'The child process is running');
 $tries = 0;
@@ -333,5 +341,5 @@ ok (!-f "$TMP/pid", 'PID file cleaned up');
 ok (!-f "$TMP/child-pid", 'Child PID file cleaned up');
 
 # Clean up.
-unlink "$TMP/krb5cc_child", "$TMP/child-out";
+unlink "$TMP/krb5cc_child", "$TMP/child-out", "$TMP/test.keytab";
 rmdir $TMP;
index 341545fcf29c558aec0d33a91eda5013861f7ccb..a7658dd4d0fb31f772dc2d46e0b72e9bf9c2b648 100755 (executable)
@@ -3,11 +3,13 @@
 # Tests for basic krenew functionality.
 #
 # Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
 # Copyright 2008, 2009, 2011
 #     The Board of Trustees of the Leland Stanford Junior University
 #
 # See LICENSE for licensing terms.
 
+use IPC::Open3 qw(open3);
 use Test::More;
 
 # The full path to the newly-built krenew client.
@@ -108,12 +110,31 @@ is ($status, 1, 'krenew -H fails with no ticket cache');
 is ($out, '', ' with no output');
 like ($err, qr/^krenew: error reading ticket cache: /, ' and the right error');
 
-# We should still exit with the same error if -i was given.
-($out, $err, $status) = command ($KRENEW, '-vi');
+# If -i was given, we keep running and keep trying and have to be explicitly
+# killed.
+my $pid = open3 ('<&0', \*OUT, \*ERR, $KRENEW, '-vi')
+    or BAIL_OUT ("can't run $KRENEW: $!");
+select (undef, undef, undef, 1.0);
+kill (15, $pid);
+$out = <OUT>;
+$err = <ERR>;
+close OUT;
+close ERR;
+waitpid ($pid, 0);
+$status = ($? >> 8);
 is ($status, 1, 'krenew -i fails with no ticket cache');
-is ($out, '', ' with no output');
+is ($out, undef, ' with no output');
 like ($err, qr/^krenew: error reading ticket cache: /, ' and the right error');
-($out, $err, $status) = command ($KRENEW, '-viH', 5);
+$pid = open3 ('<&0', \*OUT, \*ERR, $KRENEW, '-viH', 5)
+    or BAIL_OUT ("can't run $KRENEW: $!");
+select (undef, undef, undef, 1.0);
+kill (15, $pid);
+$out = <OUT>;
+$err = <ERR>;
+close OUT;
+close ERR;
+waitpid ($pid, 0);
+$status = ($? >> 8);
 is ($status, 1, 'krenew -iH fails with no ticket cache');
-is ($out, '', ' with no output');
+is ($out, undef, ' with no output');
 like ($err, qr/^krenew: error reading ticket cache: /, ' and the right error');