---
title: 'Security Configuration'
source: 'https://academia.sh/en/courses/nosql/security-configuration'
course: 'Non-Relational Data Models'
language: en
updated: '2026-08-23T07:00:46+00:00'
license: 'CC BY-SA 4.0'
---

# Security Configuration

Measuring authorization as a setting: counting the collections and records reachable in a deployment with authentication turned off, running the same five application identities under cluster-wide, database-level, and collection-level roles and comparing the totals of readable, writable, and personal-data-carrying records, naming the residue that least privilege cannot shrink, and pulling out the cost of transport and at-rest encryption as extra bytes and an extra processing step.

The previous lesson ran the same cluster under the same split across four settings and counted
the classification as a property of the request's path, not the store. In those runs, every write
that reached the cluster was either accepted or rejected, every read either answered or left
unanswered. Not once was it asked who the request came from.

This lesson asks that question, and its answer is a configuration too. Authentication mechanics
are not covered here; the subject is the store's own settings. Three things are measured: what a
closed-off authorization setting is exposed to, how much narrowing role scope shrinks it, and
which line item encryption adds cost to, and how much.

## Exposed Surface Is a Countable Quantity

The adjectives used to describe a security configuration — strict, loose, adequate — carry no
decision. Its measurable counterpart is the **exposed surface**: the total of records the
identities defined in a configuration can read. Two more quantities stand beside it: writable
records, and readable records that carry personal data. Its inputs are collection sizes, the
identities the store recognizes, and the scope of the role assigned to each identity; the first
two come from the deployment, the third is a decision.

## The Mechanism

**NS31 — the deployment has seven collections, 154,540 records, and five application
identities; each identity's job touches a specific handful of collections.** Reason: staff and
audit collections are added to the catalog, member, loan, and fine collections used throughout
the course; a search service, a loan endpoint, a fine job, a report job, and a backup job are a
typical breakdown. **NS32 — in transport encryption, a frame carries at most 16,384 bytes and
takes 29 bytes of overhead; connection setup is 5,200 bytes per connection.** Reason: the
overhead is a header, a sequence counter, and an authentication tag; what determines the result
is not the number itself but that it is **independent of response size**. **NS33 — at-rest
encryption works on 4,096-byte blocks, keeps 28 bytes of metadata per block, and every block read
is one extra decryption step.**

The mechanism is a **model**: there is no real deployment, network, or identity provider. There
is no measured number in the classes themselves; NS31–NS33 are assumptions, and every number in
the output is a calculation derived from them.

```js
// security/model.mjs — the store CONFIGURATION is the model: no real deployment, network, or
// identity provider exists. Role scope and encryption cost are computed from the parameters.
export const COLLECTION = [
  // database, collection, records, bytes per record, carries personal data?
  ["library", "book", 20000, 368, false],
  ["library", "member", 12000, 214, true],
  ["library", "loan", 20000, 96, true],
  ["library", "fine", 4800, 72, true],
  ["library", "summary", 1560, 88, false],
  ["ops", "staff", 180, 260, true],
  ["ops", "audit", 96000, 128, false],
];
export const IDENTITY = ["catalog-search", "loan-endpoint", "fine-processing", "report", "backup"];
const grant = (db, coll, ...actions) => ({ db, coll, actions });
const ALL = [grant("*", "*", "read", "write")];
const sameForAll = (r) => Object.fromEntries(IDENTITY.map((k) => [k, r]));
const DB_LEVEL = [grant("library", "*", "read", "write")];
const LEAST = {
  "catalog-search": [grant("library", "book", "read")],
  "loan-endpoint": [grant("library", "book", "read"), grant("library", "member", "read"),
    grant("library", "loan", "read", "write")],
  "fine-processing": [grant("library", "loan", "read"), grant("library", "fine", "read", "write")],
  report: [grant("library", "loan", "read"), grant("library", "fine", "read")],
  backup: [grant("*", "*", "read")],
};
export const CONFIG = [
  { name: "C0 authentication off", identity: "no", role: sameForAll(ALL) },
  { name: "C1 cluster-wide single role", identity: "yes", role: sameForAll(ALL) },
  { name: "C2 database-level role", identity: "yes", role: { ...sameForAll(DB_LEVEL), backup: LEAST.backup } },
  { name: "C3 collection-level (least privilege)", identity: "yes", role: LEAST },
  { name: "C4 collection-level + summary collection", identity: "yes",
    role: { ...LEAST, report: [grant("library", "summary", "read")] } },
];

const matches = (i, d) => (i.db === "*" || i.db === d[0]) && (i.coll === "*" || i.coll === d[1]);

export function surface(y) {
  const row = IDENTITY.map((k) => {
    let read = 0, write = 0, personal = 0;
    for (const d of COLLECTION) {
      const applicable = y.role[k].filter((i) => matches(i, d));
      if (applicable.some((i) => i.actions.includes("read"))) { read += d[2]; if (d[4]) personal += d[2]; }
      if (applicable.some((i) => i.actions.includes("write"))) write += d[2];
    }
    return { k, read, write, personal };
  });
  const total = (a) => row.reduce((s, r) => s + r[a], 0);
  const single = row.filter((r) => r.k !== "backup").map((r) => r.read);
  return { row, read: total("read"), write: total("write"), personal: total("personal"),
    widestSingle: Math.max(...single) };
}

export const FRAME = 16384, FRAME_OVERHEAD = 29, HANDSHAKE = 5200, BLOCK = 4096, BLOCK_OVERHEAD = 28;
export const frameCount = (b) => Math.ceil(b / FRAME);
export const blockCount = (b) => Math.ceil(b / BLOCK);
export const bytes = (d) => d[2] * d[3];
export const TOTAL_BYTES = COLLECTION.reduce((s, d) => s + bytes(d), 0);
export const TOTAL_BLOCKS = COLLECTION.reduce((s, d) => s + blockCount(bytes(d)), 0);
const SUMMARY_BYTES = bytes(COLLECTION.find((d) => d[1] === "summary"));
export const WORKLOAD = [
  // name, request count, bytes carried per request, blocks touched per request
  ["catalog-search point lookup", 180000, 1472, 1],
  ["loan-endpoint point lookup", 20000, 640, 1],
  ["fine-processing point lookup", 4800, 208, 1],
  ["report summary scan", 12, SUMMARY_BYTES, blockCount(SUMMARY_BYTES)],
  ["nightly backup", 1, TOTAL_BYTES, TOTAL_BLOCKS],
];
```

```js
// security/measure.mjs — same collections, same five identities, five configurations; then the
// same workload's transport and at-rest encryption cost
import { COLLECTION, IDENTITY, CONFIG, WORKLOAD, surface, frameCount, blockCount, bytes,
  FRAME_OVERHEAD, HANDSHAKE, BLOCK_OVERHEAD, TOTAL_BYTES, TOTAL_BLOCKS } from "./model.mjs";

const n = (x) => String(x).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const s = (x, w) => n(x).padStart(w);
const pct = (x) => (x * 100).toFixed(2);
const RECORDS = COLLECTION.reduce((a, d) => a + d[2], 0);

console.log(`${COLLECTION.length} collections, ${n(RECORDS)} records, ${n(TOTAL_BYTES)} bytes, ` +
  `${IDENTITY.length} application identities; the record columns are totaled across five identities.\n`);
console.log("configuration                              | identity | readable | widest single | writable | personal");
console.log("--------------------------------------------|----------|----------|----------------|----------|---------");
for (const cf of CONFIG) {
  const r = surface(cf);
  console.log(`${cf.name.padEnd(44)} | ${cf.identity.padEnd(8)} | ${s(r.read, 8)} | ` +
    `${s(r.widestSingle, 14)} | ${s(r.write, 8)} | ${s(r.personal, 7)}`);
}
const bk = CONFIG.map((cf) => surface(cf).row.find((r) => r.k === "backup"));
console.log(`\nRecords the backup identity reads across the five configurations: ` +
  `${bk.map((r) => n(r.read)).join(", ")} (personal ${n(bk[4].personal)}).`);

const requests = WORKLOAD.reduce((a, w) => a + w[1], 0);
const load = WORKLOAD.reduce((a, w) => a + w[1] * w[2], 0);
const frameEk = WORKLOAD.reduce((a, w) => a + w[1] * frameCount(w[2]) * FRAME_OVERHEAD, 0);
console.log(`\nThe same day's workload: ${n(requests)} requests, ${n(load)} bytes carried.`);
console.log("workload                     | requests | request bytes | frame overhead | ratio  | decrypt steps");
console.log("------------------------------|----------|----------------|-----------------|--------|--------------");
for (const [name, count, b, bl] of WORKLOAD) {
  const ek = frameCount(b) * FRAME_OVERHEAD;
  console.log(`${name.padEnd(29)} | ${s(count, 8)} | ${s(b, 14)} | ${s(count * ek, 15)} | ` +
    `${("%" + pct(ek / b)).padStart(6)} | ${s(count * bl, 13)}`);
}
console.log(`${"total".padEnd(29)} | ${s(requests, 8)} | ${s(load, 14)} | ${s(frameEk, 15)} | ` +
  `${("%" + pct(frameEk / load)).padStart(6)} | ${s(WORKLOAD.reduce((a, w) => a + w[1] * w[3], 0), 13)}`);

const POOL = 64, book = COLLECTION.find((d) => d[1] === "book");
console.log(`\nConnection setup ${n(HANDSHAKE)} bytes: with a ${POOL}-connection pool, ` +
  `${n(POOL * HANDSHAKE)} bytes (%${pct(POOL * HANDSHAKE / load)}); with one connection per ` +
  `request, ${n(requests * HANDSHAKE)} bytes (%${pct(requests * HANDSHAKE / load)}).`);
console.log(`At-rest encryption: ${n(TOTAL_BLOCKS)} blocks x ${BLOCK_OVERHEAD} bytes = ` +
  `${n(TOTAL_BLOCKS * BLOCK_OVERHEAD)} bytes overhead (%${pct(TOTAL_BLOCKS * BLOCK_OVERHEAD / TOTAL_BYTES)}); ` +
  `a single point read decrypts 1 block, scanning the book collection decrypts ${n(blockCount(bytes(book)))}.`);
console.log(`\nindependent of the run: the frame overhead is a fixed ${FRAME_OVERHEAD} bytes, its ratio` +
  ` depends on response size — %${pct(FRAME_OVERHEAD / 208)} on a 208-byte response, ` +
  `%${pct(FRAME_OVERHEAD / 16384)} on a full frame, ${(16384 / 208).toFixed(0)}x.`);
```

```
7 collections, 154,540 records, 24,665,680 bytes, 5 application identities; the record columns are totaled across five identities.

configuration                              | identity | readable | widest single | writable | personal
--------------------------------------------|----------|----------|----------------|----------|---------
C0 authentication off                        | no       |  772,700 |        154,540 |  772,700 | 184,900
C1 cluster-wide single role                  | yes      |  772,700 |        154,540 |  772,700 | 184,900
C2 database-level role                       | yes      |  387,980 |         58,360 |  233,440 | 184,180
C3 collection-level (least privilege)        | yes      |  276,140 |         52,000 |   24,800 | 118,580
C4 collection-level + summary collection     | yes      |  252,900 |         52,000 |   24,800 |  93,780

Records the backup identity reads across the five configurations: 154,540, 154,540, 154,540, 154,540, 154,540 (personal 36,980).

The same day's workload: 204,813 requests, 305,071,440 bytes carried.
workload                     | requests | request bytes | frame overhead | ratio  | decrypt steps
------------------------------|----------|----------------|-----------------|--------|--------------
catalog-search point lookup   |  180,000 |          1,472 |       5,220,000 |  %1.97 |       180,000
loan-endpoint point lookup    |   20,000 |            640 |         580,000 |  %4.53 |        20,000
fine-processing point lookup  |    4,800 |            208 |         139,200 | %13.94 |         4,800
report summary scan           |       12 |        137,280 |           3,132 |  %0.19 |           408
nightly backup                |        1 |     24,665,680 |          43,674 |  %0.18 |         6,024
total                         |  204,813 |    305,071,440 |       5,986,006 |  %1.96 |       211,232

Connection setup 5,200 bytes: with a 64-connection pool, 332,800 bytes (%0.11); with one connection per request, 1,065,027,600 bytes (%349.11).
At-rest encryption: 6,024 blocks x 28 bytes = 168,672 bytes overhead (%0.68); a single point read decrypts 1 block, scanning the book collection decrypts 1,797.

independent of the run: the frame overhead is a fixed 29 bytes, its ratio depends on response size — %13.94 on a 208-byte response, %0.18 on a full frame, 79x.
```

## Authenticating Does Not Shrink the Surface, Scope Does

The first table's first two rows are identical: 772,700 readable, 772,700 writable, 184,900
personal records. Turning authentication on did not shrink the exposed surface by a single
record.

The table does not show the difference, because it holds identity count fixed. Under C0 that
count is not five: every client reaching the store behaves like one of these five rows, deletion
included. C0's exposed surface is not 772,700 — it is the reachable-client count times 154,540.
Authentication is not the setting that shrinks the surface; it is the setting that binds it to a
**finite set of identities**. The shrinking is done by scope.

The three levels of scope give three numbers: the database level brings readable records down to
387,980, the collection level to 276,140. The real difference is in writes — 772,700 → 233,440 →
24,800, a 31.2x factor — because only two of the five identities need to write at all.

The `widest single` column shows what a single compromised identity would see: 154,540 → 58,360 →
52,000. But that column leaves the backup identity out; the row below it shows the backup's read
count staying at 154,540 across all five configurations, 36,980 of it personal. Least privilege
did not change the worst case — it confined it to a single identity, one whose job is to read
everything.

C4 shows the final step: when the report job is given a summary collection carrying no personal
fields, readable records drop to 252,900, personal records to 93,780. Where narrowing
authorization ends, narrowing the **data** begins.

## Encryption's Cost Is Fixed, Its Ratio Is Not

The second table computes transport encryption over the same day's workload: the overall ratio is
1.96 percent, 5,986,006 bytes against 305,071,440. The spread is wide — 13.94 percent on the
208-byte fine response, 0.18 percent on the 24,665,680-byte backup transfer. The frame overhead is
fixed at 29 bytes; what sets the ratio is not the encryption but how small the response is, and
the last line gives this independent of the run: 79x.

The real line item is below the table. Connection setup is 5,200 bytes per connection: with a
64-connection pool, 332,800 bytes a day, 0.11 percent of the load. The same workload opening a new
connection per request runs to 1,065,027,600 bytes, 349 percent of the data carried. What makes
transport encryption look expensive is usually how connections get established, not the
encryption itself.

At-rest encryption is a separate line item: 6,024 blocks × 28 bytes = 168,672 bytes, 0.68 percent
of the data, independent of data size. The cost at rest is small; the real cost is a processing
step, and it is measured not by record count but by **blocks touched**: 1 for a point read, 1,797
for a scan of the book collection. The day's 204,813 requests produce 211,232 decryption steps,
6,024 of them from a single nightly backup.

## Summary

- Exposed surface is a countable quantity: the total records defined identities can read,
  alongside the totals of writable and personal records.
- Turning authentication on did not shrink the exposed surface at all (772,700 → 772,700); that
  setting binds the surface to a finite set of identities. With it off, the exposed surface is the
  number of reachable clients times 154,540, writes and deletes included.
- Narrowing scope from a cluster-wide role to the collection level brought readable records down
  to 276,140 and writable records down to 24,800 (a 31.2x factor).
- Least privilege did not change the worst case: the backup identity read all 154,540 records
  under every one of the five configurations. Where narrowing authorization ends, narrowing data
  begins — giving the report job a summary collection brought personal records from 118,580 to
  93,780.
- Transport encryption's frame overhead is fixed at 29 bytes: its ratio is 13.94 percent on a
  208-byte response, 0.18 percent on a full frame. Connection setup without a pool adds 349
  percent of the data carried.
- At-rest encryption costs 0.68 percent at rest; its processing cost is the number of blocks
  touched — 1 extra decryption step for a point read, 1,797 for a scan of the book collection.

## Course Wrap-Up

This course asked one question across nineteen lessons: when the same work is run under two
models or two decisions, what is the measured difference, and which guarantee was given up.

| Lesson | Two models/decisions compared | Measured difference | Guarantee given up |
|---|---|---|---|
| Key-Value Stores | key access — index | penalty query 1 — 20,000 round trips | a condition on the field |
| Document Databases | document — relational | nested record 1 — 4 round trips | write-time validation |
| Wide-Column Stores | row layout — column-family layout | circulation summary 1,867 — 46 pages | one layout serving two access patterns |
| Graph Databases | join — traversal | at the fifth degree, 14,190,839 — 217,544 | cost's independence from degree |
| Time Series Stores | full timestamp — delta encoding | 9,331,200 — 329,013 bytes | the access unit becomes the segment |
| Model Selection Criteria | single pattern — mix | the winner changes with the mix | scoring accounting for writes |
| Document Data Types | explicit — short name — fixed schema | 7,357,008 — 6,082,574 — 3,139,138 bytes | the field name being stored |
| Embedding and Referencing | embedded — referenced schema | branch query 6,149,233 — 781,096 bytes | the book page's single round trip |
| Query Operators | document-level — element-level conjunction | 6,033 — 3,340 documents | conditions being met within the same element |
| Aggregation Pipeline | filter first — group first | peak memory 2,418 — 40,276 | freedom of stage order |
| Indexing | intersection — element-level compound index | 6,033 — 3,340 documents read | maintenance 10,840 — 173,994 entries |
| Schema Validation | shallow — full rule, `reject` mode | 2,500 — 3,200 documents caught | ordinary writes going through |
| Transactions | single document — multi-document transaction | 0/20 — 12/20 violations, 18 aborts | abort-freedom, a 604,903-byte undo image |
| Replica Sets | three-/four-member — five-member cluster | 12 turns without writes, 9 — 6 rolled back | the durability of an accepted write |
| Sharding | `borrow_day` — `hash(borrow_day)` — `member_id` | hot share 100% — 59.6% | balance preventing a hot shard |
| Read and Write Concerns | `w=1` — `w=majority` — `w=all` | ack turns 0/10/16, lost writes 3/0/0 | write availability |
| Consistency Models | sticky routing — session token | causal breaks 24 → 12 → 0 | stale reads staying at 47/108 |
| CAP and PACELC | `w=majority`·majority — `w=1`·local | 16 rejected/16 unanswered — 0 unanswered | the durability of an acceptance in the minority |
| Security Configuration | cluster-wide role — least privilege | writable 772,700 — 24,800 | a single identity handling every job |

The two right-hand columns are this course's rule: it is not the family that gets chosen, it is
the **access pattern**; every model buys a performance gain by giving up a guarantee. Picking a
family by name is not a decision — the decision is writing down which work gets how many times
cheaper and which guarantee drops. In none of the nineteen rows is a gain free.

These nineteen rows share one more assumption, never stated. The key-value store wrote its value
to disk, the wide-column store flushed its in-memory table to a file, the replica set acknowledged
only after the write reached disk, and this lesson's at-rest encryption encrypted exactly that
disk. Stores keeping all their data in memory, where durability itself turns into a setting, were
never taken up. The next course — **In-Memory Stores and Caching Systems** — starts from there.
