BYOC database security conversations usually stall on the same question: is this deployment secure? Asked like that, the question has no answer. A ClickHouse cluster or a PostgreSQL fleet running in your own AWS account under a vendor's control plane is neither secure nor insecure in the abstract. It is conformant, or not, to a standard you wrote down. If the standard exists and every control in it is measurable, then a sentence like "analytics-ch-prod is 77.4% conformant to MDB-SEC-2026-09-BYOC v1.0, non-conformant on one gate" is a fact you can defend to an auditor, a CISO and the vendor. Without the standard, "secure" is an opinion.
This post shows how to write that BYOC database security standard so it can be scored, how to collect evidence from the database and the cloud account rather than from a questionnaire, and how to turn the evidence into a percentage and a verdict that behave sensibly. Everything below is runnable. The evaluator output shown later is a real run against a hand-built evidence file, not a mock-up.
What BYOC actually splits, and why BYOC database security has to be scored differently
Bring Your Own Cloud (BYOC) is a specific deployment model, and the security argument hinges on its shape. The vendor's control plane, meaning the orchestrator, the tenant console, billing and support tooling, runs in the vendor's account. The data plane, meaning the database nodes, the block storage, the object storage holding data and backups, the network boundary and the KMS keys, runs in your account. ClickHouse describes its BYOC data plane as running entirely in your cloud account with the control plane in ClickHouse's VPC; Redpanda's BYOC architecture follows the same split, and so do most of the other vendors offering the model.
The bridge between the two halves is a cross-account IAM role the control-plane agent assumes, plus whatever break-glass path the vendor's operators use when something needs hands.
That split is what makes BYOC database security scoreable at all. In a fully managed service you can only attest: read the SOC 2 report, accept the shared-responsibility matrix, move on. In BYOC, every object in the data plane is queryable through your own cloud APIs and your own database catalog. You can measure it, and if you can measure it you can score it. The control plane stays opaque, and the standard has to be honest about that boundary rather than pretending a SOC 2 PDF is the same kind of evidence as a security-group rule you read from the API.
Writing a BYOC database security standard that produces a number
Most BYOC database security policies fail the measurability test for a mundane reason: their controls are sentences, not predicates. "Access to production databases must be appropriately restricted" cannot be scored. A control becomes scoreable when it has four properties. It is atomic, testing exactly one thing. It binds to exactly one evidence key, a value some collector can produce without a human interpreting it. It carries an expectation expressed as an operator and a value. And it carries a weight, because a wildcard IAM policy on the control-plane agent and a SOC 2 report that is thirteen months old are not the same size of problem.
The BYOC database security standard below has seven domains and eighteen controls. It is deliberately short. A standard with two hundred controls, half of which nobody can collect evidence for, produces a number nobody trusts. Eighteen controls with real collectors produce a number you can put in a board pack. Add controls only when a collector exists for them.
# MDB-SEC-2026-09-BYOC -- BYOC database security standard, v1.0
# Every control is atomic, has exactly one evidence key, and carries a weight.
# gate: true means a FAIL caps the whole target at "non-conformant" regardless of score.
standard: MDB-SEC-2026-09-BYOC
version: "1.0"
weights: {critical: 5, high: 3, medium: 1}
domains:
- id: IAM
name: Identity and access
controls:
- id: IAM-01
title: No shared superuser / default admin account enabled for application use
severity: critical
gate: true
evidence: db.superuser_logins_last_30d
expect: {op: eq, value: 0}
- id: IAM-02
title: Database authentication is federated (IAM / OIDC / cert), not password-only
severity: high
evidence: db.password_only_roles
expect: {op: eq, value: 0}
- id: IAM-03
title: Vendor cross-account role uses an ExternalId and is scoped to the data-plane account only
severity: critical
gate: true
evidence: cloud.vendor_role_external_id_enforced
expect: {op: eq, value: true}
- id: NET
name: Network boundary
controls:
- id: NET-01
title: Database endpoints have no 0.0.0.0/0 ingress on any port
severity: critical
gate: true
evidence: cloud.public_ingress_rules
expect: {op: eq, value: 0}
- id: NET-02
title: Client access only through PrivateLink / Private Service Connect / peering
severity: high
evidence: cloud.private_connectivity_only
expect: {op: eq, value: true}
- id: NET-03
title: Egress from the data plane is allow-listed (vendor control-plane CIDRs + object storage only)
severity: high
evidence: cloud.egress_allowlisted
expect: {op: eq, value: true}
- id: ENC
name: Encryption
controls:
- id: ENC-01
title: Storage volumes and object-storage buckets encrypted with a customer-managed key
severity: critical
gate: true
evidence: cloud.cmk_encryption
expect: {op: eq, value: true}
- id: ENC-02
title: TLS 1.2+ enforced on every client-facing listener
severity: high
evidence: db.tls_min_version
expect: {op: gte, value: 1.2}
- id: ENC-03
title: Backups encrypted with the same or a dedicated CMK, never vendor-default keys
severity: high
evidence: cloud.backup_cmk
expect: {op: eq, value: true}
- id: CPL
name: Control-plane access
controls:
- id: CPL-01
title: Vendor operator access to the data plane is break-glass, ticketed, and time-boxed
severity: high
evidence: vendor.breakglass_ttl_hours
expect: {op: lte, value: 8}
- id: CPL-02
title: Control-plane agent runs with least-privilege IAM (no iam:*, no kms:ScheduleKeyDeletion)
severity: critical
gate: true
evidence: cloud.agent_policy_wildcards
expect: {op: eq, value: 0}
- id: CPL-03
title: Vendor SOC 2 Type II report reviewed within the last 12 months
severity: medium
evidence: vendor.soc2_age_days
expect: {op: lte, value: 365}
- id: LOG
name: Audit and logging
controls:
- id: LOG-01
title: Database audit log (DDL, grants, auth failures) shipped to a SIEM the vendor cannot write to
severity: high
evidence: db.audit_log_shipped
expect: {op: eq, value: true}
- id: LOG-02
title: Cloud API audit trail (CloudTrail / Audit Logs) enabled for the data-plane account
severity: high
evidence: cloud.api_audit_enabled
expect: {op: eq, value: true}
- id: BKP
name: Backup and recovery
controls:
- id: BKP-01
title: Backups stored in a bucket the vendor control plane cannot delete
severity: critical
gate: true
evidence: cloud.backup_bucket_vendor_delete_denied
expect: {op: eq, value: true}
- id: BKP-02
title: Restore drill executed and evidenced within the last 90 days
severity: high
evidence: ops.last_restore_drill_days
expect: {op: lte, value: 90}
- id: CFG
name: Configuration hardening
controls:
- id: CFG-01
title: No database-level access from the public internet (listen / pg_hba / ClickHouse listen_host)
severity: high
evidence: db.listens_public
expect: {op: eq, value: false}
- id: CFG-02
title: Version within vendor support window and no unpatched CVE older than 30 days
severity: medium
evidence: db.oldest_unpatched_cve_days
expect: {op: lte, value: 30}
Three design choices in that file matter more than the specific controls, and they are what make BYOC database security measurable rather than merely documented. First, the gate: true flag. Six controls are gates; a failure on any of them makes the target non-conformant regardless of the percentage. Without gates, weighted scoring produces the pathology where a deployment with an internet-exposed listener still reads as "81%, mostly fine".
Second, every control's evidence key is a dotted path into a JSON document, so the standard never references a database engine, a cloud provider or a vendor by name. The same file scores a ClickHouse BYOC cluster on AWS and a PostgreSQL BYOC deployment on GCP; only the collectors differ. Third, the file has an ID and a version in the MinervaDB document convention, MDB-SEC-2026-09-BYOC v1.0, because a score is meaningless unless the reader knows which standard it was scored against. Changing a weight changes every historical number, so weights change through a versioned pull request, never in place.
Collecting BYOC database security evidence from the database, not from a questionnaire
The evidence for the db.* keys in a BYOC database security assessment comes from the database's own catalog. The point of writing BYOC database security collectors as SQL is that the SQL is the audit trail: anyone can rerun the query and get the same answer. Two collectors follow, one for ClickHouse and one for PostgreSQL, each producing the raw values the standard expects. Version pinning matters here; several of these system tables changed shape recently.
ClickHouse BYOC database security collector (24.8 LTS and later)
-- BYOC database security: ClickHouse collector for db.* keys. Tested syntax on ClickHouse 24.8 LTS and later.
-- Run as a user holding SHOW USERS, SHOW GRANTS and access to system.query_log.
-- IAM-01 logins as the built-in `default` user in the last 30 days
-- (query_log is on by default; session_log is not, so we count from query_log)
SELECT count() AS superuser_logins_last_30d
FROM system.query_log
WHERE user = 'default'
AND type = 'QueryStart'
AND event_date >= today() - INTERVAL 30 DAY;
-- IAM-02 roles that can still log in with a password only
-- auth_type became an Array when multiple auth methods per user
-- landed (24.9+); on older builds drop the hasAny() and compare directly
SELECT count() AS password_only_roles
FROM system.users
WHERE hasAny(auth_type,
['plaintext_password', 'sha256_password',
'double_sha1_password', 'bcrypt_password'])
AND NOT hasAny(auth_type, ['ldap', 'kerberos', 'ssl_certificate', 'jwt', 'http']);
-- CFG-01 is the server listening on anything but private / loopback interfaces?
-- (system.server_settings, 23.x+)
SELECT name, value, changed
FROM system.server_settings
WHERE name IN ('listen_host', 'tcp_port', 'http_port',
'tcp_port_secure', 'https_port');
-- ENC-02 plaintext listeners left open count as a FAIL for TLS enforcement
SELECT countIf(name IN ('tcp_port', 'http_port') AND value != '') AS plaintext_listeners
FROM system.server_settings;
-- CPL-01 / audit trail grants issued outside the change window
SELECT event_time, user, query
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_kind IN ('Grant', 'Revoke', 'Create', 'Drop', 'Alter')
AND event_date >= today() - INTERVAL 7 DAY
ORDER BY event_time DESC
LIMIT 50;
Two notes on these queries. system.query_log is the right place to count logins by the built-in default user because it is on by default and system.session_log usually is not; if you enable session_log you get a cleaner signal, including failed logins. And the auth_type column on system.users became an array when multiple authentication methods per user landed, so the hasAny() form is the one that survives upgrades. On BYOC deployments you typically cannot edit config.xml directly, but you can read system.server_settings, and that is enough to score the listener posture.
PostgreSQL BYOC database security collector (16 and later)
-- BYOC database security: PostgreSQL collector for db.* keys. Tested syntax on PostgreSQL 16 and 17.
-- Run as a role holding pg_read_all_settings and pg_read_all_stats.
-- IAM-01 login-capable superusers other than the bootstrap role
SELECT count(*) AS extra_login_superusers
FROM pg_roles
WHERE rolsuper
AND rolcanlogin
AND rolname <> 'postgres';
-- IAM-02 pg_hba entries that authenticate with a password or nothing at all
-- (federated methods: cert, gss, sspi, ldap, radius, oauth in 18+)
SELECT count(*) AS password_only_rules
FROM pg_hba_file_rules
WHERE auth_method IN ('trust', 'password', 'md5', 'scram-sha-256')
AND error IS NULL;
-- CFG-01 / ENC-02 listener and TLS posture in one pass
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN ('listen_addresses', 'ssl', 'ssl_min_protocol_version',
'log_connections', 'password_encryption');
-- LOG-01 is pgaudit loaded and shipping DDL / role changes?
SELECT name, setting
FROM pg_settings
WHERE name IN ('shared_preload_libraries', 'pgaudit.log', 'log_destination');
pg_hba_file_rules is the collector that most often surprises people. It shows the rules PostgreSQL actually loaded, including ones with parse errors, so filter on error IS NULL or you will count rules that are not in effect. On PostgreSQL 18 the oauth method joins the federated list. A superuser that can log in and is not the bootstrap role is the PostgreSQL analogue of the ClickHouse default user problem: it is where application credentials end up when nobody is looking.
Cloud account BYOC database security collector (AWS shown, read-only)
The cloud.* keys are where BYOC database security differs from every other model, because the objects that matter live in your account. The collector below reads security groups, the vendor's cross-account role trust policy, the agent role's attached policies, EBS key management and CloudTrail state. It runs under a read-only audit role and writes nothing. The GCP and Azure equivalents are structurally identical: replace the IAM trust-policy check with a Workload Identity binding or a service-principal check, and the EBS/KMS check with CMEK on persistent disks or managed disks.
#!/usr/bin/env python3
"""
collect_aws.py -- BYOC database security: produce the cloud.* evidence keys for one BYOC data-plane account.
Run with a READ-ONLY role in the data-plane account:
AWS_PROFILE=byoc-audit python3 collect_aws.py \
--vendor-role ClickHouseBYOCControlPlane \
--tag Key=byoc-target,Value=analytics-ch-prod > evidence/cloud.json
The script only reads. It writes nothing to AWS.
"""
import argparse
import json
import boto3
WILDCARD_DENYLIST = {"iam:*", "kms:*", "kms:ScheduleKeyDeletion", "s3:*", "ec2:*", "*"}
def public_ingress_rules(ec2, tag_key, tag_val) -> int:
"""NET-01: security-group ingress rules open to the world on data-plane ENIs."""
sgs = ec2.describe_security_groups(
Filters=[{"Name": f"tag:{tag_key}", "Values": [tag_val]}])["SecurityGroups"]
hits = 0
for sg in sgs:
for rule in sg["IpPermissions"]:
hits += sum(1 for r in rule.get("IpRanges", []) if r["CidrIp"] == "0.0.0.0/0")
hits += sum(1 for r in rule.get("Ipv6Ranges", []) if r["CidrIpv6"] == "::/0")
return hits
def vendor_role_external_id_enforced(iam, role_name) -> bool:
"""IAM-03: the trust policy must carry a StringEquals on sts:ExternalId."""
doc = iam.get_role(RoleName=role_name)["Role"]["AssumeRolePolicyDocument"]
for stmt in doc["Statement"]:
cond = stmt.get("Condition", {}).get("StringEquals", {})
if "sts:ExternalId" not in cond:
return False
return True
def agent_policy_wildcards(iam, role_name) -> int:
"""CPL-02: count denylisted wildcard actions across inline + attached policies."""
docs = []
for name in iam.list_role_policies(RoleName=role_name)["PolicyNames"]:
docs.append(iam.get_role_policy(RoleName=role_name, PolicyName=name)["PolicyDocument"])
for att in iam.list_attached_role_policies(RoleName=role_name)["AttachedPolicies"]:
ver = iam.get_policy(PolicyArn=att["PolicyArn"])["Policy"]["DefaultVersionId"]
docs.append(iam.get_policy_version(PolicyArn=att["PolicyArn"], VersionId=ver)["PolicyVersion"]["Document"])
hits = 0
for doc in docs:
for stmt in doc["Statement"]:
if stmt.get("Effect") != "Allow":
continue
actions = stmt["Action"] if isinstance(stmt["Action"], list) else [stmt["Action"]]
hits += sum(1 for a in actions if a in WILDCARD_DENYLIST)
return hits
def cmk_encryption(ec2, kms, tag_key, tag_val) -> bool:
"""ENC-01: every tagged EBS volume encrypted with a key whose KeyManager is CUSTOMER."""
vols = ec2.describe_volumes(Filters=[{"Name": f"tag:{tag_key}", "Values": [tag_val]}])["Volumes"]
if not vols:
return False
for v in vols:
if not v["Encrypted"]:
return False
if kms.describe_key(KeyId=v["KmsKeyId"])["KeyMetadata"]["KeyManager"] != "CUSTOMER":
return False
return True
def api_audit_enabled(ct) -> bool:
"""LOG-02: at least one multi-region trail that is logging and log-file-validated."""
for t in ct.describe_trails()["trailList"]:
if t.get("IsMultiRegionTrail") and t.get("LogFileValidationEnabled"):
if ct.get_trail_status(Name=t["TrailARN"])["IsLogging"]:
return True
return False
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--vendor-role", required=True)
ap.add_argument("--tag", required=True, help="Key=<k>,Value=<v> identifying data-plane resources")
a = ap.parse_args()
tag_key, tag_val = [kv.split("=", 1)[1] for kv in a.tag.split(",")]
ec2, iam, kms, ct = (boto3.client(s) for s in ("ec2", "iam", "kms", "cloudtrail"))
print(json.dumps({"cloud": {
"public_ingress_rules": public_ingress_rules(ec2, tag_key, tag_val),
"vendor_role_external_id_enforced": vendor_role_external_id_enforced(iam, a.vendor_role),
"agent_policy_wildcards": agent_policy_wildcards(iam, a.vendor_role),
"cmk_encryption": cmk_encryption(ec2, kms, tag_key, tag_val),
"api_audit_enabled": api_audit_enabled(ct),
}}, indent=2))
The IAM-03 check deserves emphasis. A vendor cross-account role whose trust policy lacks a sts:ExternalId condition is the textbook confused-deputy setup: any tenant of that vendor who learns your role ARN can ask the vendor's control plane to act on it. Every serious BYOC vendor issues an ExternalId during onboarding. The control exists because onboarding scripts get copied between accounts, and the condition gets dropped.
The BYOC database security scoring engine
The evaluator that turns evidence into a BYOC database security score is short on purpose. Its rules are the interesting part, and each one is a decision about what the number should mean. The score is the weighted share of applicable controls that pass.
A control whose evidence key is absent is UNKNOWN and scores as a fail; unproven is treated as unsafe, which is the only rule that stops "we did not run the collector" from inflating the number. A control the evidence marks as not applicable leaves the denominator entirely. And any failing gate control caps the verdict at NON-CONFORMANT while still reporting the score, so the percentage remains useful for tracking progress even when the verdict is red.
#!/usr/bin/env python3
"""
byoc_score.py -- BYOC database security: score one target against a control standard.
python3 byoc_score.py byoc_security_standard.yaml evidence/analytics-ch-prod.json
Rules the number is built on (keep these stable; changing them changes history):
* score = sum(weight of PASS) / sum(weight of applicable controls)
* a control with missing evidence is UNKNOWN and scores as FAIL -- unproven is unsafe
* a control marked not_applicable in evidence is removed from the denominator
* any gate control that fails caps the verdict at NON-CONFORMANT, whatever the score
"""
import json
import operator
import sys
from collections import defaultdict
import yaml
OPS = {"eq": operator.eq, "ne": operator.ne, "gte": operator.ge, "lte": operator.le}
def lookup(evidence: dict, dotted: str):
"""evidence['db']['tls_min_version'] for 'db.tls_min_version'; None if absent."""
node = evidence
for part in dotted.split("."):
if not isinstance(node, dict) or part not in node:
return None
node = node[part]
return node
def evaluate(standard: dict, evidence: dict) -> dict:
weights = standard["weights"]
results, by_domain = [], defaultdict(lambda: {"earned": 0, "possible": 0})
earned = possible = 0
gate_failures = []
for domain in standard["domains"]:
for ctl in domain["controls"]:
w = weights[ctl["severity"]]
value = lookup(evidence, ctl["evidence"])
if value == "not_applicable":
status = "N/A"
elif value is None:
status = "UNKNOWN"
else:
exp = ctl["expect"]
status = "PASS" if OPS[exp["op"]](value, exp["value"]) else "FAIL"
if status != "N/A":
possible += w
by_domain[domain["id"]]["possible"] += w
if status == "PASS":
earned += w
by_domain[domain["id"]]["earned"] += w
elif ctl.get("gate"):
gate_failures.append(ctl["id"])
results.append({"id": ctl["id"], "domain": domain["id"], "severity": ctl["severity"],
"weight": w, "status": status, "observed": value,
"expect": ctl["expect"], "title": ctl["title"]})
score = round(100 * earned / possible, 1) if possible else 0.0
verdict = "NON-CONFORMANT" if gate_failures else ("CONFORMANT" if score >= 90 else "PARTIAL")
return {"standard": f'{standard["standard"]} v{standard["version"]}',
"target": evidence.get("target", "?"),
"score": score, "verdict": verdict, "gate_failures": gate_failures,
"domains": {d: round(100 * v["earned"] / v["possible"], 1) if v["possible"] else None
for d, v in by_domain.items()},
"controls": results}
def print_report(rep: dict) -> None:
print(f'{rep["target"]} vs {rep["standard"]}')
print(f'score {rep["score"]}% verdict {rep["verdict"]}'
+ (f' (gates failed: {", ".join(rep["gate_failures"])})' if rep["gate_failures"] else ""))
print("-" * 78)
for c in rep["controls"]:
flag = " GATE" if c["status"] == "FAIL" and c["id"] in rep["gate_failures"] else ""
print(f'{c["id"]:7} {c["status"]:8} w={c["weight"]} observed={c["observed"]!r:<12} {c["title"][:44]}{flag}')
print("-" * 78)
for d, s in rep["domains"].items():
print(f'{d:5} {s if s is not None else "n/a":>6}%')
if __name__ == "__main__":
with open(sys.argv[1]) as f:
std = yaml.safe_load(f)
with open(sys.argv[2]) as f:
ev = json.load(f)
report = evaluate(std, ev)
print_report(report)
with open(sys.argv[2].replace(".json", ".report.json"), "w") as f:
json.dump(report, f, indent=2)
Here is a hand-built BYOC database security evidence file for a ClickHouse BYOC cluster. The values are illustrative; the file shape is exactly what the collectors above produce once merged. Note the empty ops block: nobody recorded a restore drill, and that absence is going to be scored.
{
"target": "analytics-ch-prod (ClickHouse BYOC, AWS eu-central-1)",
"collected_at": "2026-09-03T10:14:00Z",
"db": {
"superuser_logins_last_30d": 0,
"password_only_roles": 2,
"tls_min_version": 1.2,
"audit_log_shipped": true,
"listens_public": false,
"oldest_unpatched_cve_days": 12
},
"cloud": {
"vendor_role_external_id_enforced": true,
"public_ingress_rules": 0,
"private_connectivity_only": true,
"egress_allowlisted": false,
"cmk_encryption": true,
"backup_cmk": true,
"agent_policy_wildcards": 1,
"api_audit_enabled": true,
"backup_bucket_vendor_delete_denied": true
},
"vendor": {
"breakglass_ttl_hours": 4,
"soc2_age_days": 210
},
"ops": {}
}
And the run, unedited:
$ python3 byoc_score.py byoc_security_standard.yaml evidence/analytics-ch-prod.json analytics-ch-prod (ClickHouse BYOC, AWS eu-central-1) vs MDB-SEC-2026-09-BYOC v1.0 score 77.4% verdict NON-CONFORMANT (gates failed: CPL-02) ------------------------------------------------------------------------------ IAM-01 PASS w=5 observed=0 No shared superuser / default admin account IAM-02 FAIL w=3 observed=2 Database authentication is federated (IAM / IAM-03 PASS w=5 observed=True Vendor cross-account role uses an ExternalId NET-01 PASS w=5 observed=0 Database endpoints have no 0.0.0.0/0 ingress NET-02 PASS w=3 observed=True Client access only through PrivateLink / Pri NET-03 FAIL w=3 observed=False Egress from the data plane is allow-listed ( ENC-01 PASS w=5 observed=True Storage volumes and object-storage buckets e ENC-02 PASS w=3 observed=1.2 TLS 1.2+ enforced on every client-facing lis ENC-03 PASS w=3 observed=True Backups encrypted with the same or a dedicat CPL-01 PASS w=3 observed=4 Vendor operator access to the data plane is CPL-02 FAIL w=5 observed=1 Control-plane agent runs with least-privileg GATE CPL-03 PASS w=1 observed=210 Vendor SOC 2 Type II report reviewed within LOG-01 PASS w=3 observed=True Database audit log (DDL, grants, auth failur LOG-02 PASS w=3 observed=True Cloud API audit trail (CloudTrail / Audit Lo BKP-01 PASS w=5 observed=True Backups stored in a bucket the vendor contro BKP-02 UNKNOWN w=3 observed=None Restore drill executed and evidenced within CFG-01 PASS w=3 observed=False No database-level access from the public int CFG-02 PASS w=1 observed=12 Version within vendor support window and no ------------------------------------------------------------------------------ IAM 76.9% NET 72.7% ENC 100.0% CPL 44.4% LOG 100.0% BKP 62.5% CFG 100.0%
Read the output the way the CISO will. The headline is not 77.4%; it is NON-CONFORMANT with CPL-02 named as the reason, meaning the vendor's control-plane agent holds a wildcard action in your account. That single line is the conversation to have with the vendor this week.
The 77.4% is the second sentence, and it decomposes: two password-only ClickHouse roles, egress from the data plane not allow-listed, and a restore drill nobody can evidence. The domain breakdown shows CPL at 44.4% and BKP at 62.5%, which is where the next quarter's work goes. Encryption, logging and configuration hardening are at 100% and can be left alone.
What a BYOC database security percentage means, and what it does not
A BYOC database security conformance percentage is a statement about a standard, not about risk. Two things follow for anyone reporting BYOC database security upward. First, the number is only as good as the weights, and weights are a policy decision, so publish them and version them; a team that quietly changes critical from 5 to 3 to hit a target has broken the metric, not improved security. Second, the number can be gamed by removing controls, which is why the standard's control count and version travel with every score. "92% against v1.0" and "92% against v1.3" are different claims, and the report.json carries both.
There is also a boundary the standard has to admit. The vendor.* keys are attestations: a SOC 2 report's age, a break-glass TTL the vendor documented. You are recording that the vendor said something, not measuring it. That is why those controls carry lower weights and no gates. If a vendor offers an API for its break-glass audit log, and some do, promote that control from attestation to measurement and raise its weight in the next version of the standard. The direction of travel for BYOC database security is precisely that: moving controls from the hatched region of Fig. 1 into the measurable one.
Running BYOC database security scoring continuously
A BYOC database security score taken once during onboarding is a photograph. The value of a measurable standard comes from running it on a schedule and treating a drop as an incident. The pipeline in Fig. 2 runs nightly from CI and on two additional triggers: any change to the standard file, and any change in the vendor's agent version, because agent upgrades are when IAM policies quietly grow. Store each report.json with its timestamp, plot the score per target, and alert when it decreases. A decrease means something in your account changed, or the vendor changed something in your account, and in a BYOC deployment those are the two things you most want to know about.
Open one BYOC database security ticket per FAIL and per UNKNOWN, tagged with the control ID, and close them by re-running the evaluator rather than by hand. UNKNOWN BYOC database security tickets are usually the cheapest to close and the most revealing: they are the controls nobody has ever actually checked.
Where to start with BYOC database security scoring
If you run one BYOC database today, you can have a first BYOC database security score by the end of the week. Take the standard above as v0.1, delete the controls you cannot yet collect evidence for, run the SQL and cloud collectors by hand into a single evidence file, and score it. Then add BYOC database security collectors back one at a time. The first number will be low and the first verdict will probably be NON-CONFORMANT; that is the standard doing its job. A deployment you thought was fine and a deployment you can prove is 77.4% conformant with one named gate failure are different things, and only one of them survives contact with an auditor.
MinervaDB handles BYOC database security reviews for teams running PostgreSQL, ClickHouse, MySQL, MongoDB and Kafka across all three clouds under vendor-neutral 24×7 consultative support and remote DBA engagements, and building a scoreable BYOC database security standard for a specific vendor and estate is a common first deliverable. If you want the collectors extended to your platform, or a review of a standard you have already written, book a working session.