diff --git a/cmd.py b/cmd.py index 3badaec..af3d964 100755 --- a/cmd.py +++ b/cmd.py @@ -36,7 +36,7 @@ class CliWrapper(CliWrapper): PATH = [ dirname(realpath(__file__)) ] COMMANDS_USAGE_ORDER = ['init', '', - 'passphrase', 'escrow', + 'passphrase', 'escrow', 'set-fallback', '', 'backup', 'list', 'restore', 'restore-rollback', '', diff --git a/cmd_backup.py b/cmd_backup.py index 0b5fa36..7ce6619 100755 --- a/cmd_backup.py +++ b/cmd_backup.py @@ -342,15 +342,24 @@ def main(): except hb.Error, e: # asking for get_credentials() might fail if the hub is down. # But If we already have the credentials we can survive that. - if isinstance(e, hub.NotSubscribed) or \ not registry.credentials or \ + (registry.credentials and hub.credentials_expired(registry.credentials)) or \ registry.credentials.type == 'iamrole': - fatal(e) - - warn("using cached backup credentials: " + e.description) - - credentials = registry.credentials + # if we don't have cached credentials we might still be + # able to use the fallback credentials + if registry.fallback_access_key: + credentials = hub.Credentials.IAMUser( + registry.fallback_access_key, + registry.fallback_secret_key, + '') + warn("using fallback credentials: " + e.description) + else: + fatal(e) + + else: + warn("using cached backup credentials: " + e.description) + credentials = registry.credentials if registry.hbr: try: diff --git a/cmd_init.py b/cmd_init.py index dc69743..8da3015 100755 --- a/cmd_init.py +++ b/cmd_init.py @@ -166,6 +166,7 @@ def main(): registry.registry.sub_apikey = None registry.registry.credentials = None else: + if force or not registry.registry.sub_apikey: if not apikey: print "Copy paste the API-KEY from your Hub account's user profile" diff --git a/cmd_set_fallback.py b/cmd_set_fallback.py new file mode 100755 index 0000000..550451b --- /dev/null +++ b/cmd_set_fallback.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# Copyright (c) 2018 TurnKey GNU/Linux - https://www.turnkeylinux.org +# +# This file is part of TKLBAM (TurnKey GNU/Linux BAckup and Migration). +# +# TKLBAM is open source 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. +# +""" +Set Fallback + +This is used to setup fallback credentials used when access to the Hub is limited +or not possible. + +Arguments: + + FALLBACK-ACCESS-KEY Your IAMUser access key + + FALLBACK-SECRET-KEY Your IAMUser secret access key + +Security warning: + + Providing your AWS IAMUser credentials as a commandline argument is + potentially less secure than allowing tklbam-set-fallback to prompt + you for it interactively: + + * The shell may save the APIKEY to its history file (e.g., ~/.bash_history) + * The FALLBACK-ACCESS-KEY and FALLBACK-SECRET-KEY may briefly show up in the + process list. + + AWS IAMUser credentials must exist on your system in plaintext as they are + intended to work even when you can only access your AWS bucket and should + work automatically without interaction. + +""" + +import sys +import registry +import getopt + +def usage(e=None): + if e: + print >> sys.stderr, "error: " + str(e) + + print >> sys.stderr, "Usage: %s [ -options ]" % sys.argv[0] + print >> sys.stderr, __doc__.strip() + + sys.exit(1) + +def main(): + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], "h", ["help"]) + except getopt.GetoptError, e: + usage(e) + + fallback_access_key = None + fallback_secret_key = None + + for opt, val in opts: + if opt in ('-h', '--help'): + usage() + + if args: + if len(args) > 2: + usage() + elif len(args) == 1: + fallback_access_key = args[0] + elif len(args) == 2: + fallback_access_key = args[0] + fallback_secret_key = args[1] + else: + usage() + + if not fallback_access_key: + print "Copy paste the AWS access key" + + while True: + fallback_access_key = raw_input("ACCESS-KEY: ").strip() + if fallback_access_key: + break + if not fallback_secret_key: + print "Copy paste the AWS secret access key" + + while True: + fallback_secret_key = raw_input("SECRET-KEY: ").strip() + if fallback_secret_key: + break + + registry.registry.fallback_access_key = fallback_access_key + registry.registry.fallback_secret_key = fallback_secret_key + +if __name__ == '__main__': + main() diff --git a/docs/tklbam-faq.txt b/docs/tklbam-faq.txt index 717d417..eebc2c0 100644 --- a/docs/tklbam-faq.txt +++ b/docs/tklbam-faq.txt @@ -474,6 +474,10 @@ No, for a couple of reasons: 2) You can use TKLBAM without linking it to the Hub at all. See the tklbam-init --solo option. +3) You can setup fallback credentials to your Amazon S3. Even if the Hub goes + down AND your cached credentials become invalidated your backups can still + function as normal. + If the Hub goes down, will my backup cron jobs still work? ---------------------------------------------------------- @@ -481,6 +485,9 @@ Yes. Backups which have already been configured will continue to work normally. If TKLBAM can't reach the Hub it just uses the locally cached profile and S3 address. +In the event that locally cached credentials become invalidated you can also +setup fallback keys to S3 as an additional protective measure. + If my connection to the Hub goes down, can I still restore? ----------------------------------------------------------- diff --git a/docs/tklbam-set-fallback.txt b/docs/tklbam-set-fallback.txt new file mode 100644 index 0000000..112feab --- /dev/null +++ b/docs/tklbam-set-fallback.txt @@ -0,0 +1,46 @@ +=================== +tklbam-set-fallback +=================== + +------------ +Set Fallback +------------ + +:Author: Stefan Davis +:Date: 2018-10-09 +:Manual section: 8 +:Manual group: backup + +SYNOPSIS +======== + +tklbam-set-fallback [ `FALLBACK-ACCESS-KEY` ] [ `FALLBACK-SECRET-KEY` ] + +This is used to setup fallback credentials used when access to the Hub is limited +or not possible. + +ARGUMENTS +========= + +`FALLBACK-ACCESS-KEY` Your IAMUser access key +`FALLBACK-SECRET-KEY` Your IAMUser secret access key + +If you do not provide FALLBACK-ACCESS-KEY and/or FALLBACK-SECRET-KEY you will be +prompted for it/them interactively. + +SECURITY WARNING +================ + +Providing your AWS IAMUser credentials as a commandline argument is potentially less secure +than allowing tklbam-set-fallback to prompt you for it interactively. + +* The shell may save the credentials in its history file (e.g. ~/.bash_history) +* The credentials may briefly show up in the process list. + +AWS IAMUser credentials exist on your system in plaintext. This is to allow backups to work +seamlessly without interaction. + +SEE ALSO +======== + +``tklbam`` (8), ``tklbam-faq`` (7) diff --git a/docs/tklbam.txt b/docs/tklbam.txt index b92b729..7f12833 100644 --- a/docs/tklbam.txt +++ b/docs/tklbam.txt @@ -151,6 +151,7 @@ COMMANDS :list: List backup records :restore: Restore a backup :restore-rollback: Rollback last restore +:set-fallback: Sets additional fallback keys for S3 EXAMPLE USAGE SCENARIO ====================== diff --git a/hub.py b/hub.py index c02790b..2a51f11 100644 --- a/hub.py +++ b/hub.py @@ -208,6 +208,10 @@ def from_dict(cls, d): return(creds_types[creds_type](**kwargs)) +def credentials_expired(credentials): + ''' Checks if credentials object has expired, expects credentials to be non None ''' + return datetime.strptime(credentials['expiration'], '%Y-%m-%dT%H:%M:%SZ') <= datetime.now() + class Backups: API_URL = os.getenv('TKLBAM_APIURL', 'https://hub.turnkeylinux.org/api/backup/') Error = Error diff --git a/registry.py b/registry.py index 1ac7c2d..444fbfa 100644 --- a/registry.py +++ b/registry.py @@ -67,7 +67,8 @@ class ProfileNotFound(Exception): class Paths(_Paths): files = ['restore.log', 'backup.log', 'backup.pid', 'backup-resume', 'sub_apikey', 'secret', 'key', 'credentials', 'hbr', - 'profile', 'profile/stamp', 'profile/profile_id'] + 'profile', 'profile/stamp', 'profile/profile_id', 'fallback_access_key', + 'fallback_secret_key'] def __init__(self, path=None): if path is None: @@ -115,6 +116,14 @@ def _file_dict(cls, path, d=UNDEFINED): if retval: return AttrDict([ v.split("=", 1) for v in retval.split("\n") ]) + def fallback_access_key(self, val=UNDEFINED): + return self._file_str(self.path.fallback_access_key, val) + fallback_access_key = property(fallback_access_key, fallback_access_key) + + def fallback_secret_key(self, val=UNDEFINED): + return self._file_str(self.path.fallback_secret_key, val) + fallback_secret_key = property(fallback_secret_key, fallback_secret_key) + def sub_apikey(self, val=UNDEFINED): return self._file_str(self.path.sub_apikey, val) sub_apikey = property(sub_apikey, sub_apikey)