]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
samba-tool: Filter confidential attributes out of backups made with the ‘--no-secrets...
authorJennifer Sutton <jennifersutton@catalyst.net.nz>
Tue, 28 Jan 2025 01:15:02 +0000 (14:15 +1300)
committerJo Sutton <jsutton@samba.org>
Mon, 26 May 2025 02:41:36 +0000 (02:41 +0000)
Without this change, ‘lab domains’ and backups intended not to contain
secrets will still contain confidential information, such as BitLocker
recovery data and KDS root keys. Add a new class that filters these
attributes out.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=15852

Signed-off-by: Jennifer Sutton <jennifersutton@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
python/samba/drs_utils.py
python/samba/join.py
selftest/knownfail.d/domain-backup-no-secrets [deleted file]

index ab65767d1bada54f21ffb723b48462ebe12eb31f..61c0576726fe3a0c417550ce3305acf128477c9f 100644 (file)
@@ -478,3 +478,82 @@ class drs_ReplicateRenamer(drs_ReplicatorImplBase):
 
     def get_nc_changes(self, req_level, req) -> bool:
         return self.repl.get_nc_changes(req_level, req)
+
+
+class drs_SecretFilter(drs_ReplicatorImplBase):
+    # Objects of these types contain sensitive attributes that cannot simply be
+    # filtered out, because they are required attributes for their class
+    # (mustContain and systemMustContain). Instead of filtering out those
+    # sensitive attributes, we filter out the entire object.
+    object_classes_to_filter_out = {
+        "msKds-ProvRootKey",
+        "msFVE-RecoveryInformation",
+        "msTPM-InformationObject",
+    }
+
+    def __init__(self, repl):
+        self.repl = repl
+
+    def process_chunk(self, samdb, level, ctr, schema, req_level, req, first_chunk):
+        """Processes a single chunk of received replication data"""
+
+        def get_searchFlags(attr: drsuapi.DsReplicaAttribute) -> int:
+            attr_name = samdb.get_lDAPDisplayName_by_attid(attr.attid)
+            return samdb.get_searchFlags_from_lDAPDisplayName(attr_name)
+
+        def is_confidential(attr: drsuapi.DsReplicaAttribute) -> bool:
+            return get_searchFlags(attr) & dsdb.SEARCH_FLAG_CONFIDENTIAL
+
+        prev_obj = ctr
+        obj = ctr.first_object
+        first = True
+        while obj is not None:
+            object_class = None
+            must_contain = set()
+            for attr in obj.object.attribute_ctr.attributes:
+                if (
+                    attr.attid == drsuapi.DRSUAPI_ATTID_objectClass
+                    and attr.value_ctr.num_values
+                ):
+                    object_class = samdb.get_lDAPDisplayName_by_governsID_id(
+                        int.from_bytes(
+                            attr.value_ctr.values[0].blob, byteorder="little"
+                        )
+                    )
+                    must_contain = samdb.get_must_contain_from_lDAPDisplayName(
+                        object_class
+                    )
+                    break
+
+            if object_class in self.object_classes_to_filter_out:
+                obj = obj.next_object
+                if first:
+                    prev_obj.first_object = obj
+                else:
+                    prev_obj.next_object = obj
+
+                ctr.object_count -= 1
+                continue
+
+            for attr in filter(is_confidential, obj.object.attribute_ctr.attributes):
+                attr_name = samdb.get_lDAPDisplayName_by_attid(attr.attid)
+                if attr_name in must_contain:
+                    print(
+                        f"Warning: {attr_name} is a required attribute of {object_class} "
+                        f"— not filtering with --no-secrets"
+                    )
+                else:
+                    attr.value_ctr.num_values = 0
+
+            prev_obj = obj
+            obj = obj.next_object
+            first = False
+
+        # then do the normal repl processing to apply this chunk to our DB
+        self.repl.process_chunk(samdb, level, ctr, schema, req_level, req, first_chunk)
+
+    def supports_ext(self, ext) -> bool:
+        return self.repl.supports_ext(ext)
+
+    def get_nc_changes(self, req_level, req) -> bool:
+        return self.repl.get_nc_changes(req_level, req)
index 3ee3bdac690afbc2d6f93cfde7c7efb4d59e9b76..0da5af6addb9bece229bf9d41f0e0ed8e0679678 100644 (file)
@@ -66,7 +66,8 @@ class DCJoinContext(object):
                  promote_existing=False, plaintext_secrets=False,
                  backend_store=None,
                  backend_store_size=None,
-                 forced_local_samdb=None):
+                 forced_local_samdb=None,
+                 filter_secrets=False):
 
         ctx.logger = logger
         ctx.creds = creds
@@ -77,6 +78,7 @@ class DCJoinContext(object):
         ctx.plaintext_secrets = plaintext_secrets
         ctx.backend_store = backend_store
         ctx.backend_store_size = backend_store_size
+        ctx.filter_secrets = filter_secrets
 
         ctx.promote_existing = promote_existing
         ctx.promote_from_dn = None
@@ -961,6 +963,8 @@ class DCJoinContext(object):
             ctx.local_samdb,
             ctx.invocation_id,
         )
+        if ctx.filter_secrets:
+            repl = drs_utils.drs_SecretFilter(repl)
         return repl
 
     def join_replicate(ctx):
@@ -1664,7 +1668,8 @@ class DCCloneContext(DCJoinContext):
                          targetdir=targetdir, domain=domain,
                          dns_backend=dns_backend,
                          backend_store=backend_store,
-                         backend_store_size=backend_store_size)
+                         backend_store_size=backend_store_size,
+                         filter_secrets=not include_secrets)
 
         # As we don't want to create or delete these DNs, we set them to None
         ctx.server_dn = None
diff --git a/selftest/knownfail.d/domain-backup-no-secrets b/selftest/knownfail.d/domain-backup-no-secrets
deleted file mode 100644 (file)
index 7f7161d..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-^samba\.tests\.domain_backup\.samba\.tests\.domain_backup\.DomainBackupOnline\.test_backup_restore_no_secrets\(ad_dc_backup:local\)$
-^samba\.tests\.domain_backup\.samba\.tests\.domain_backup\.DomainBackupRename\.test_backup_restore_no_secrets\(ad_dc_backup:local\)$
-^samba\.tests\.domain_backup\.samba\.tests\.domain_backup\.DomainBackupOnline\.test_backup_restore_no_secrets\(restoredc:local\)$
-^samba\.tests\.domain_backup\.samba\.tests\.domain_backup\.DomainBackupRename\.test_backup_restore_no_secrets\(restoredc:local\)$