Recognize empty provider templates during deployment preflight
Some checks failed
Build and verify web migration / verify (push) Has been cancelled

This commit is contained in:
wolf-demon 2026-09-21 09:15:47 +01:00
parent 39751c5f1a
commit 98628ab01f
3 changed files with 18 additions and 1 deletions

View File

@ -49,6 +49,12 @@ def image_id(image):
return run(["docker", "image", "inspect", "--format", "{{.Id}}", image]).decode().strip()
def provider_configured(config, target):
section = "Pms" if target == "/run/guestops/pms.json" else "Payments"
# Only the exact shipped empty template is public; unknown settings fail closed.
return config not in ({}, {section: {"Hotels": {}}})
AUTH = 'const c=new Mongo("mongodb://127.0.0.1"); c.getDB("admin").auth(process.env.MONGO_INITDB_ROOT_USERNAME,process.env.MONGO_INITDB_ROOT_PASSWORD);'
INVENTORY = 'const d=c.getDB("guestops"); print(JSON.stringify({bytes:d.stats().storageSize+d.stats().indexSize,collections:Object.fromEntries(d.getCollectionNames().filter(n=>!n.startsWith("system.")).sort().map(n=>[n,{count:d.getCollection(n).countDocuments({}),indexes:d.getCollection(n).getIndexes().map(i=>{delete i.ns;return i;}).sort((a,b)=>a.name.localeCompare(b.name))}]))}));'
@ -94,7 +100,7 @@ def preflight(args):
require(Path(volume["source"]).exists(), f"A required {name} bind mount is missing.")
if volume["target"] in ("/run/guestops/pms.json", "/run/guestops/payments.json"):
provider = Path(volume["source"])
if any(json.loads(provider.read_text()).values()):
if provider_configured(json.loads(provider.read_text()), volume["target"]):
require(stat.S_IMODE(provider.stat().st_mode) & 0o077 == 0, "Configured provider files must not be accessible to group or other users.")
if not args.offline:
url = config["services"]["api"]["environment"]["PublicUrl"]
@ -249,3 +255,4 @@ if __name__ == "__main__":
# Never include subprocess output, config values or parsed guest data.
print(str(error) if isinstance(error, RuntimeError) else "Operation failed (" + type(error).__name__ + "). No sensitive details were logged.", file=sys.stderr)
sys.exit(1)

View File

@ -14,6 +14,8 @@ python3 deploy/ops.py preflight
Preflight checks production settings, shared persistent keys, unpublished MongoDB/worker ports, a loopback API port, required images and mounts, private secret files, and at least 8 GiB free disk. The online check also verifies the configured HTTPS readiness URL. This is not a firewall, capacity or provider acceptance test. Allow additional disk space for the database, images and temporary backup/restore files; the supplied server initially had 18 GiB free.
Configured provider files must be readable by the API container's `app` user while inaccessible to other users. Set their ownership to that image's application UID and mode 600, and run the backup as an authorized operator able to read them (for example root with its designated public GPG keyring). Keep their parent directory private. Empty shipped example files contain no credentials and do not need this ownership change. Check the UID in the reviewed image rather than assuming it matches your host login.
## Encrypted backup
Keep the recovery private key on an administrator-controlled recovery machine, with a securely stored passphrase and a second protected recovery copy. Import only its public key on the server and verify its full 40-character fingerprint through a trusted channel. The tool selects that exact fingerprint; it does not establish who owns the key. Do not put private keys, decrypted backups or provider secrets in GitHub.
@ -68,3 +70,4 @@ Restore into new isolated MongoDB and key volumes; preserve the damaged original
**A restored database can predate emails, invoices and PMS changes that providers already completed.** Review pending, sending and uncertain records against provider evidence before enabling any worker, including automatic FAQ rules. Do not replay an older approval merely because the restored record says it is pending. Reconcile external effects, validate account sessions and mailbox authorization, and explicitly approve the cutover only after these checks. Rotate credentials if compromise prompted the recovery. Keep the old deployment stopped when enabling the replacement.
CI exercises a synthetic encrypted backup and isolated restore drill, including actual key decryption and database comparison. A successful CI drill is separate from the required rehearsal on the Debian server with its actual deployment configuration.

View File

@ -16,6 +16,12 @@ spec.loader.exec_module(ops)
class ArchiveTests(unittest.TestCase):
def test_empty_provider_templates_are_not_secret_configuration(self):
for section, target in (("Pms", "/run/guestops/pms.json"), ("Payments", "/run/guestops/payments.json")):
self.assertFalse(ops.provider_configured({section: {"Hotels": {}}}, target))
self.assertTrue(ops.provider_configured({section: {"Hotels": {"fixture": {"Key": "fixture"}}}}, target))
self.assertTrue(ops.provider_configured({section: {"Unknown": "fixture"}}, target))
def bundle(self, root, change=None):
files = {name: b"fixture backup data" for name in ops.FILES - {"manifest.json"}}
files["manifest.json"] = json.dumps({"format": 1, "sha256": {name: hashlib.sha256(data).hexdigest() for name, data in files.items()}}).encode()
@ -88,3 +94,4 @@ class ArchiveTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()