mirror of https://github.com/borgbackup/borg.git
Merge pull request #8159 from ThomasWaldmann/repo-upgrade-helper-1.2
repo upgrade helpers (1.2-maint)
This commit is contained in:
commit
f001aaa3e1
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: '(src|scripts|conftest.py)'
|
||||
|
|
|
@ -29,49 +29,63 @@ places. Borg now considers archives without TAM as garbage or an attack.
|
|||
|
||||
We are not aware of others having discovered, disclosed or exploited this vulnerability.
|
||||
|
||||
Below, if we speak of borg 1.2.6, we mean a borg version >= 1.2.6 **or** a
|
||||
borg version that has the relevant security patches for this vulnerability applied
|
||||
Below, if we speak of borg 1.2.8, we mean a borg version >= 1.2.8 **or** a
|
||||
borg version that has the relevant patches for this vulnerability applied
|
||||
(could be also an older version in that case).
|
||||
|
||||
Steps you must take to upgrade a repository (this applies to all kinds of repos
|
||||
no matter what encryption mode they use, including "none"):
|
||||
|
||||
1. Upgrade all clients using this repository to borg 1.2.6.
|
||||
1. Upgrade all clients using this repository to borg 1.2.8.
|
||||
Note: it is not required to upgrade a server, except if the server-side borg
|
||||
is also used as a client (and not just for "borg serve").
|
||||
|
||||
Do **not** run ``borg check`` with borg 1.2.6 before completing the upgrade steps:
|
||||
Do **not** run ``borg check`` with borg > 1.2.4 before completing the upgrade steps:
|
||||
|
||||
- ``borg check`` would complain about archives without a valid archive TAM.
|
||||
- ``borg check --repair`` would remove such archives!
|
||||
2. Run: ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg info --debug <repo> 2>&1 | grep TAM | grep -i manifest``
|
||||
2. Do this step on every client using this repo: ``borg upgrade --show-rc --check-tam <repo>``
|
||||
|
||||
a) If you get "TAM-verified manifest", continue with 3.
|
||||
b) If you get "Manifest TAM not found and not required", run
|
||||
``borg upgrade --tam --force <repository>`` *on every client*.
|
||||
This will check the manifest TAM authentication setup in the repo and on this client.
|
||||
The command will exit with rc=0 if all is OK, otherwise with rc=1.
|
||||
|
||||
3. Run: ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg list --consider-checkpoints --format='{name} {time} tam:{tam}{NL}' <repo>``
|
||||
a) If you get "Manifest authentication setup OK for this client and this repository."
|
||||
and rc=0, continue with 3.
|
||||
b) If you get some warnings and rc=1, run:
|
||||
``borg upgrade --tam --force <repository>``
|
||||
|
||||
"tam:verified" means that the archive has a valid TAM authentication.
|
||||
"tam:none" is expected as output for archives created by borg <1.0.9.
|
||||
"tam:none" is also expected for archives resulting from a borg rename
|
||||
or borg recreate operation (see #7791).
|
||||
"tam:none" could also come from archives created by an attacker.
|
||||
You should verify that "tam:none" archives are authentic and not malicious
|
||||
3. Run: ``borg upgrade --show-rc --check-archives-tam <repo>``
|
||||
|
||||
This will create a report about the TAM status for all archives.
|
||||
In the last line(s) of the report, it will also report the overall status.
|
||||
The command will exit with rc=0 if all archives are TAM authenticated or with rc=1
|
||||
if there are some archives with TAM issues.
|
||||
|
||||
If there are no issues and all archives are TAM authenticated, continue with 5.
|
||||
|
||||
Archive TAM issues are expected for:
|
||||
|
||||
- archives created by borg <1.0.9.
|
||||
- archives resulting from a borg rename or borg recreate operation (see #7791)
|
||||
|
||||
But, important, archive TAM issues could also come from archives created by an attacker.
|
||||
You should verify that archives with TAM issues are authentic and not malicious
|
||||
(== have good content, have correct timestamp, can be extracted successfully).
|
||||
In case you find crappy/malicious archives, you must delete them before proceeding.
|
||||
|
||||
In low-risk, trusted environments, you may decide on your own risk to skip step 3
|
||||
and just trust in everything being OK.
|
||||
|
||||
4. If there are no tam:none archives left at this point, you can skip this step.
|
||||
Run ``BORG_WORKAROUNDS=ignore_invalid_archive_tam borg upgrade --archives-tam <repo>``.
|
||||
4. If there are no archives with TAM issues left at this point, you can skip this step.
|
||||
|
||||
Run ``borg upgrade --archives-tam <repo>``.
|
||||
|
||||
This will unconditionally add a correct archive TAM to all archives not having one.
|
||||
``borg check`` would consider TAM-less or invalid-TAM archives as garbage or a potential attack.
|
||||
To see that all archives now are "tam:verified" run: ``borg list --consider-checkpoints --format='{name} {time} tam:{tam}{NL}' <repo>``
|
||||
|
||||
5. Please note that you should never use BORG_WORKAROUNDS=ignore_invalid_archive_tam
|
||||
for normal production operations - it is only needed once to get the archives in a
|
||||
repository into a good state. All archives have a valid TAM now.
|
||||
To see that all archives are OK now, you can optionally repeat the command from step 3.
|
||||
|
||||
5. Done. Manifest and archives are TAM authenticated now.
|
||||
|
||||
Vulnerability time line:
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ try:
|
|||
from .helpers import sig_int, ignore_sigint
|
||||
from .helpers import iter_separated
|
||||
from .helpers import get_tar_filter
|
||||
from .helpers import ignore_invalid_archive_tam
|
||||
from .helpers.parseformat import BorgJsonEncoder, safe_decode
|
||||
from .nanorst import rst_to_terminal
|
||||
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
|
||||
|
@ -1635,52 +1636,88 @@ class Archiver:
|
|||
DASHES, logger=logging.getLogger('borg.output.stats'))
|
||||
return self.exit_code
|
||||
|
||||
@with_repository(fake=('tam', 'disable_tam', 'archives_tam'), invert_fake=True, manifest=False, exclusive=True)
|
||||
@with_repository(fake=('tam', 'check_tam', 'disable_tam', 'archives_tam', 'check_archives_tam'), invert_fake=True, manifest=False, exclusive=True)
|
||||
def do_upgrade(self, args, repository, manifest=None, key=None):
|
||||
"""upgrade a repository from a previous version"""
|
||||
if args.archives_tam:
|
||||
manifest, key = Manifest.load(repository, (Manifest.Operation.CHECK,), force_tam_not_required=args.force)
|
||||
with Cache(repository, key, manifest) as cache:
|
||||
stats = Statistics()
|
||||
for info in manifest.archives.list(sort_by=['ts']):
|
||||
archive_id = info.id
|
||||
archive_formatted = format_archive(info)
|
||||
cdata = repository.get(archive_id)
|
||||
data = key.decrypt(archive_id, cdata)
|
||||
archive, verified, _ = key.unpack_and_verify_archive(data, force_tam_not_required=True)
|
||||
if not verified: # we do not have an archive TAM yet -> add TAM now!
|
||||
archive = ArchiveItem(internal_dict=archive)
|
||||
archive.cmdline = [safe_decode(arg) for arg in archive.cmdline]
|
||||
data = key.pack_and_authenticate_metadata(archive.as_dict(), context=b'archive')
|
||||
new_archive_id = key.id_hash(data)
|
||||
cache.add_chunk(new_archive_id, data, stats)
|
||||
cache.chunk_decref(archive_id, stats)
|
||||
manifest.archives[info.name] = (new_archive_id, info.ts)
|
||||
print(f"Added archive TAM: {archive_formatted} -> [{bin_to_hex(new_archive_id)}]")
|
||||
if args.archives_tam or args.check_archives_tam:
|
||||
with ignore_invalid_archive_tam():
|
||||
archive_tam_issues = 0
|
||||
read_only = args.check_archives_tam
|
||||
manifest, key = Manifest.load(repository, (Manifest.Operation.CHECK,), force_tam_not_required=args.force)
|
||||
with Cache(repository, key, manifest) as cache:
|
||||
stats = Statistics()
|
||||
for info in manifest.archives.list(sort_by=['ts']):
|
||||
archive_id = info.id
|
||||
archive_formatted = format_archive(info)
|
||||
cdata = repository.get(archive_id)
|
||||
data = key.decrypt(archive_id, cdata)
|
||||
archive, verified, _ = key.unpack_and_verify_archive(data, force_tam_not_required=True)
|
||||
if not verified:
|
||||
if not read_only:
|
||||
# we do not have an archive TAM yet -> add TAM now!
|
||||
archive = ArchiveItem(internal_dict=archive)
|
||||
archive.cmdline = [safe_decode(arg) for arg in archive.cmdline]
|
||||
data = key.pack_and_authenticate_metadata(archive.as_dict(), context=b'archive')
|
||||
new_archive_id = key.id_hash(data)
|
||||
cache.add_chunk(new_archive_id, data, stats)
|
||||
cache.chunk_decref(archive_id, stats)
|
||||
manifest.archives[info.name] = (new_archive_id, info.ts)
|
||||
print(f"Added archive TAM: {archive_formatted} -> [{bin_to_hex(new_archive_id)}]")
|
||||
else:
|
||||
print(f"Archive TAM missing: {archive_formatted}")
|
||||
archive_tam_issues += 1
|
||||
else:
|
||||
print(f"Archive TAM present: {archive_formatted}")
|
||||
if not read_only:
|
||||
manifest.write()
|
||||
repository.commit(compact=False)
|
||||
cache.commit()
|
||||
if archive_tam_issues > 0:
|
||||
print(f"Fixed {archive_tam_issues} archives with TAM issues!")
|
||||
print("All archives are TAM authenticated now.")
|
||||
else:
|
||||
print("All archives are TAM authenticated.")
|
||||
else:
|
||||
print(f"Archive TAM present: {archive_formatted}")
|
||||
manifest.write()
|
||||
repository.commit(compact=False)
|
||||
cache.commit()
|
||||
elif args.tam:
|
||||
manifest, key = Manifest.load(repository, (Manifest.Operation.CHECK,), force_tam_not_required=args.force)
|
||||
if not manifest.tam_verified or not manifest.config.get(b'tam_required', False):
|
||||
print('Manifest contents:')
|
||||
for archive_info in manifest.archives.list(sort_by=['ts']):
|
||||
print(format_archive(archive_info))
|
||||
manifest.config[b'tam_required'] = True
|
||||
manifest.write()
|
||||
repository.commit(compact=False)
|
||||
if not key.tam_required and hasattr(key, 'change_passphrase'):
|
||||
key.tam_required = True
|
||||
key.change_passphrase(key._passphrase)
|
||||
print('Key updated')
|
||||
if hasattr(key, 'find_key'):
|
||||
print('Key location:', key.find_key())
|
||||
if not tam_required(repository):
|
||||
tam_file = tam_required_file(repository)
|
||||
open(tam_file, 'w').close()
|
||||
print('Updated security database')
|
||||
if archive_tam_issues > 0:
|
||||
self.print_warning(f"Found {archive_tam_issues} archives with TAM issues!")
|
||||
else:
|
||||
print("All archives are TAM authenticated.")
|
||||
elif args.tam or args.check_tam:
|
||||
with ignore_invalid_archive_tam():
|
||||
manifest_tam_issues = 0
|
||||
read_only = args.check_tam
|
||||
manifest, key = Manifest.load(repository, (Manifest.Operation.CHECK,), force_tam_not_required=args.force)
|
||||
if not manifest.tam_verified or not manifest.config.get(b'tam_required', False):
|
||||
if not read_only:
|
||||
print('Manifest contents:')
|
||||
for archive_info in manifest.archives.list(sort_by=['ts']):
|
||||
print(format_archive(archive_info))
|
||||
manifest.config[b'tam_required'] = True
|
||||
manifest.write()
|
||||
repository.commit(compact=False)
|
||||
else:
|
||||
manifest_tam_issues += 1
|
||||
self.print_warning("Repository Manifest is not TAM verified or a TAM is not required!")
|
||||
if not key.tam_required and hasattr(key, 'change_passphrase'):
|
||||
if not read_only:
|
||||
key.tam_required = True
|
||||
key.change_passphrase(key._passphrase)
|
||||
print('Key updated')
|
||||
if hasattr(key, 'find_key'):
|
||||
print('Key location:', key.find_key())
|
||||
else:
|
||||
manifest_tam_issues += 1
|
||||
self.print_warning("Key does not require TAM authentication!")
|
||||
if not tam_required(repository):
|
||||
if not read_only:
|
||||
tam_file = tam_required_file(repository)
|
||||
open(tam_file, 'w').close()
|
||||
print('Updated security database')
|
||||
else:
|
||||
manifest_tam_issues += 1
|
||||
self.print_warning("Client-side security database does not require a TAM!")
|
||||
if read_only and manifest_tam_issues == 0:
|
||||
print("Manifest authentication setup OK for this client and this repository.")
|
||||
elif args.disable_tam:
|
||||
manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK, force_tam_not_required=True)
|
||||
if tam_required(repository):
|
||||
|
@ -4996,8 +5033,12 @@ class Archiver:
|
|||
help='Force upgrade')
|
||||
subparser.add_argument('--tam', dest='tam', action='store_true',
|
||||
help='Enable manifest authentication (in key and cache) (Borg 1.0.9 and later).')
|
||||
subparser.add_argument('--check-tam', dest='check_tam', action='store_true',
|
||||
help='check manifest authentication (in key and cache).')
|
||||
subparser.add_argument('--disable-tam', dest='disable_tam', action='store_true',
|
||||
help='Disable manifest authentication (in key and cache).')
|
||||
subparser.add_argument('--check-archives-tam', dest='check_archives_tam', action='store_true',
|
||||
help='check TAM authentication for all archives.')
|
||||
subparser.add_argument('--archives-tam', dest='archives_tam', action='store_true',
|
||||
help='add TAM authentication for all archives.')
|
||||
subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
|
||||
|
|
|
@ -14,6 +14,7 @@ from ..logger import create_logger
|
|||
|
||||
logger = create_logger()
|
||||
|
||||
from .. import helpers
|
||||
from ..constants import * # NOQA
|
||||
from ..compress import Compressor
|
||||
from ..helpers import StableDict
|
||||
|
@ -24,7 +25,6 @@ from ..helpers import get_limited_unpacker
|
|||
from ..helpers import bin_to_hex
|
||||
from ..helpers import prepare_subprocess_env
|
||||
from ..helpers import msgpack
|
||||
from ..helpers import workarounds
|
||||
from ..item import Key, EncryptedKey
|
||||
from ..platform import SaveFile
|
||||
|
||||
|
@ -34,7 +34,7 @@ from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b
|
|||
|
||||
|
||||
# workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
|
||||
AUTHENTICATED_NO_KEY = 'authenticated_no_key' in workarounds
|
||||
AUTHENTICATED_NO_KEY = 'authenticated_no_key' in helpers.workarounds
|
||||
|
||||
|
||||
class NoPassphraseFailure(Error):
|
||||
|
@ -322,7 +322,7 @@ class KeyBase:
|
|||
tam_key = self._tam_key(tam_salt, context=b'archive')
|
||||
calculated_hmac = hmac.digest(tam_key, data, 'sha512')
|
||||
if not hmac.compare_digest(calculated_hmac, tam_hmac):
|
||||
if 'ignore_invalid_archive_tam' in workarounds:
|
||||
if 'ignore_invalid_archive_tam' in helpers.workarounds:
|
||||
logger.debug('ignoring invalid archive TAM due to BORG_WORKAROUNDS')
|
||||
return unpacked, False, None # same as if no TAM is present
|
||||
else:
|
||||
|
|
|
@ -5,6 +5,7 @@ that did not fit better elsewhere.
|
|||
Code used to be in borg/helpers.py but was split into the modules in this
|
||||
package, which are imported into here for compatibility.
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .checks import * # NOQA
|
||||
from .datastruct import * # NOQA
|
||||
|
@ -26,6 +27,18 @@ from . import msgpack
|
|||
# see the docs for a list of known workaround strings.
|
||||
workarounds = tuple(os.environ.get('BORG_WORKAROUNDS', '').split(','))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ignore_invalid_archive_tam():
|
||||
global workarounds
|
||||
saved = workarounds
|
||||
if 'ignore_invalid_archive_tam' not in workarounds:
|
||||
# we really need this workaround here or borg will likely raise an exception.
|
||||
workarounds += ('ignore_invalid_archive_tam',)
|
||||
yield
|
||||
workarounds = saved
|
||||
|
||||
|
||||
"""
|
||||
The global exit_code variable is used so that modules other than archiver can increase the program exit code if a
|
||||
warning or error occurred during their operation. This is different from archiver.exit_code, which is only accessible
|
||||
|
|
Loading…
Reference in New Issue