Skip to content
academia.sh

Lesson 04 / 12

Storage Exposure

Two separate measures of a publicly open storage item: 7 of 13 object groups are open, with 61,697 records and 6 of 7 data types readable. Closing public access at the bucket level closes 9,260 personal records but leaves the spread radius at 10; closing the 47-record configuration item brings the radius down from 10 to 5 and stops 9 legitimate accesses a day. Closing all public access costs 8,655 accesses a day.

Published Updated

In this lesson

  • Derives the effective permission from bucket- and object-level permissions and counts the publicly open item
  • Measures the data type and record count readable from the open item as separate exposure figures
  • Shows that the fix that lowers record volume and the fix that lowers the spread radius are not the same fix
Contents

The previous lesson treated the network path as a precondition: if the path was closed, permission did not matter. But some items have their outside path open on purpose — the subscriber portal’s asset store gets eight thousand six hundred legitimate outside accesses a day and cannot be closed. For that item the question is not the path, it is the permission set: which identity an incoming request is served with, and what a request that presents no identity at all can read.

This lesson scans the permission set of storage items. The Storage and Data in the Cloud course measured bucket access and signed links; the mechanics are not repeated here, only exposure is counted. Two separate figures come out, and the lesson’s finding is that they diverge: the records readable from an open item and the spread radius that starts from that item do not drop with the same fix.

  • CM18. Permission is written at two levels: the bucket level and the object group level. If a value is written at the object level, that is the effective permission; if not, the bucket level is inherited.
  • CM19. A publicly open item is one that a request presenting no identity at all can read or list. Signed access does not count as open in this lesson.
  • CM20. For every object group, a data type, a record count, and how many legitimate public accesses it gets a day are recorded. The numbers are fictional.
  • CM21. An object group can carry an identity reference: the file inside it contains a role’s name and the configuration that uses it. The credential itself is not modeled — only the reference’s presence and its consequence are counted.
  • CM22. The exposure radius is the union of two components: buckets read without an identity, and the items reached by the walk that starts from an identity reference in an open file.

The Permission Set and the Open Item

// cloud/storage.mjs — object storage permission set and access graph MODEL. Fictional regional
// measurement network; storage class, signed-link mechanics, and provider name are not present
// in this lesson.

// Columns: bucket, object group, bucket-level permission, object-level permission ('-' = inherits
// from the bucket), data type, record count, legitimate public accesses a day, identity reference
// carried by the file in the group.
export const GROUP = `
  portal-asset-store    site-assets           public-read public-read  asset            1400 8600 -
  portal-asset-store    upload-preview        public-read -            subscriber-personal 9260 0 -
  measurement-store     daily-upload          private     public-read  measurement     41800   40 -
  measurement-store     raw-read              private     -            measurement    512000    0 -
  billing-output-store  subscriber-invoice    private     signed       billing         24500  120 -
  billing-output-store  bulk-export           private     public-list  billing           310    0 -
  raw-archive           historical-read       private     -            measurement   1240000    0 -
  raw-archive           shared-pull           private     public-read  measurement      8700    6 -
  backup-store          nightly-backup        private     -            backup            620    0 -
  backup-store          old-backup            private     public-read  backup            180    0 -
  template-store        distribution-template private     public-read  configuration      47    9 role-distribution
  report-store          monthly-report        private     signed       billing            36   30 -
  log-store             application-log       private     -            log             88000    0 -`
  .trim().split('\n').map((s) => {
    const p = s.trim().split(/\s+/);
    return { bucket: p[0], group: p[1], bucketPerm: p[2], objectPerm: p[3], type: p[4],
      records: Number(p[5]), legitimate: Number(p[6]), identity: p[7] === '-' ? null : p[7] };
  });
export const PUBLIC = ['public-read', 'public-list'];
// Effective permission: if the object level writes a value, that governs; if not, the bucket
// level is inherited.
export const effective = (g, d = {}) => {
  const bucket = d.bucketClosed ? 'private' : g.bucketPerm;
  const object = d.closed && d.closed(g) ? 'private' : g.objectPerm;
  return object === '-' ? bucket : object;
};
export const open = (g, d) => PUBLIC.includes(effective(g, d));

// Access graph (same as the previous lessons): the walk from an identity reference found in an
// open file.
const ACCESS = {
  'account-collector': 'measurement-store measurement-queue',
  'account-validator': 'measurement-store measurement-queue raw-archive',
  'account-portal': 'subscriber-database portal-asset-store work-order-queue',
  'account-report': 'report-store billing-output-store log-store',
  'role-billing': 'billing-database billing-output-store mail-dispatch subscriber-database',
  'role-archive': 'raw-archive backup-store',
  'role-backup': 'backup-store billing-database subscriber-database measurement-store',
  'role-distribution': 'distribution-configuration template-store instance-management',
  'role-log': 'log-store audit-log',
  'role-management': 'instance-management key-management secret-store',
};
const ASSUME = [['account-collector', 'role-archive'], ['account-validator', 'role-archive'],
  ['role-archive', 'role-backup'], ['role-backup', 'role-management'], ['account-portal', 'role-billing'],
  ['role-billing', 'role-log'], ['account-report', 'role-log'], ['role-distribution', 'role-management']];
export function walk(start) {
  const g = new Set([start]), item = new Set();
  let row = [start];
  while (row.length) {
    const s = [];
    for (const k of row) {
      for (const r of ACCESS[k].split(' ')) item.add(r);
      for (const [a, b] of ASSUME) if (a === k && !g.has(b)) { g.add(b); s.push(b); }
    }
    row = s;
  }
  return item;
}
// Exposure radius: buckets read without an identity + the walk of the identity reference in an
// open file.
export function radius(d) {
  const y = new Set(GROUP.filter((g) => open(g, d)).map((g) => g.bucket));
  for (const g of GROUP.filter((x) => open(x, d) && x.identity))
    for (const r of walk(g.identity)) y.add(r);
  return y;
}
// cloud/run1.mjs — how many items are publicly open, which data type, how many records
import { GROUP, open, effective, radius } from './storage.mjs';

const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-22, -23, -22, -21, 9, 7];
print(A, 'bucket', 'object group', 'effective permission', 'data type', 'records', 'legit');
for (const g of GROUP.filter((x) => open(x)))
  print(A, g.bucket, g.group, effective(g), g.type, g.records, g.legitimate);
const records = (f) => GROUP.filter(f).reduce((a, g) => a + g.records, 0);
console.log(`\nopen item ${GROUP.filter((g) => open(g)).length}/${GROUP.length}, ` +
  `open records ${records((g) => open(g))} / total ${records(() => true)} ` +
  `(${(records((g) => open(g)) / records(() => true) * 100).toFixed(1)}%)`);
const type = [...new Set(GROUP.map((g) => g.type))];
console.log(`data type ${type.filter((t) => GROUP.some((g) => g.type === t && open(g))).length}/` +
  `${type.length} open; type left closed: ` +
  type.filter((t) => GROUP.every((g) => g.type !== t || !open(g))).join(', '));
const y = radius({});
console.log(`\nbucket read without identity ${new Set(GROUP.filter((g) => open(g)).map((g) => g.bucket)).size}` +
  `; radius with the identity reference in an open file ${y.size}`);
bucket                object group           effective permission  data type              records  legit
portal-asset-store    site-assets            public-read           asset                     1400   8600
portal-asset-store    upload-preview         public-read           subscriber-personal       9260      0
measurement-store     daily-upload           public-read           measurement              41800     40
billing-output-store  bulk-export            public-list           billing                    310      0
raw-archive           shared-pull            public-read           measurement               8700      6
backup-store          old-backup             public-read           backup                     180      0
template-store        distribution-template  public-read           configuration               47      9

open item 7/13, open records 61697 / total 1926853 (3.2%)
data type 6/7 open; type left closed: log

bucket read without identity 6; radius with the identity reference in an open file 10

Seven of thirteen object groups are publicly open. The open record count is sixty-one thousand six hundred and ninety-seven, only three point two percent of the total — the volume measure looks small. The data type measure does not say the same thing: six of seven types are open, and the only type left closed is the application log.

Four rows need to be read separately. site-assets is open on purpose and gets eight thousand six hundred legitimate accesses a day. upload-preview sits in the same bucket, inherits the bucket-level permission, and carries nine thousand two hundred and sixty subscriber personal records; it gets zero legitimate public accesses a day. The only difference between the two groups is that the second one never wrote its own permission.

daily-upload and shared-pull are the opposite case: their buckets are private, openness is written at the object level individually, and both have a legitimate justification. old-backup and bulk-export are also open at the object level but get no legitimate access at all — meaning a justification once existed, and when the justification lapsed, the permission did not.

The last row gives the lesson’s second measure. A request with no identity reaches six buckets. The distribution template inside template-store carries an identity reference, and the walk that starts from that reference brings the radius up to ten. Four items — distribution configuration, instance management, key management, and secret store — were never opened by any storage permission; they come from a forty-seven-record configuration file.

Five Fixes

// cloud/run2.mjs — five fixes: open items, open records, radius, and stopped legitimate access
import { GROUP, open, radius } from './storage.mjs';

const FIX = [
  ['no audit', {}],
  ['public access closes at the bucket level', { bucketClosed: true }],
  ['items with 0 legitimate public access close', { closed: (g) => g.legitimate === 0 }],
  ['the open item of type configuration closes', { closed: (g) => g.type === 'configuration' }],
  ['all public access closes', { bucketClosed: true, closed: () => true }],
];
const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-43, 7, 9, 9, 11];
print(A, 'fix', 'open', 'open', 'radius', 'stopped job');
print(A, '', 'item', 'record', '', 'a day');
for (const [name, d] of FIX) {
  const a = GROUP.filter((g) => open(g, d));
  print(A, name, a.length, a.reduce((x, g) => x + g.records, 0), radius(d).size,
    GROUP.filter((g) => open(g) && !open(g, d)).reduce((x, g) => x + g.legitimate, 0));
}
console.log('\nthe two components of the radius (buckets read without identity / items added from the reference):');
for (const [name, d] of FIX) {
  const bucket = new Set(GROUP.filter((g) => open(g, d)).map((g) => g.bucket)).size;
  console.log(`  ${name.padEnd(43)}${String(bucket).padStart(2)} / ${radius(d).size - bucket}`);
}
const dropped = [...radius({})].filter((r) => !radius(FIX[3][1]).has(r)).sort();
console.log(`\nitems dropped from the radius when the configuration-type item closes ${dropped.length}:`);
console.log('  ' + dropped.join(', '));
fix                                           open     open   radiusstopped job
                                              item   record               a day
no audit                                         7    61697       10          0
public access closes at the bucket level         6    52437       10          0
items with 0 legitimate public access close      4    51947        8          0
the open item of type configuration closes       6    61650        5          9
all public access closes                         0        0        0       8655

the two components of the radius (buckets read without identity / items added from the reference):
  no audit                                    6 / 4
  public access closes at the bucket level    6 / 4
  items with 0 legitimate public access close 4 / 4
  the open item of type configuration closes  5 / 0
  all public access closes                    0 / 0

items dropped from the radius when the configuration-type item closes 5:
  distribution-configuration, instance-management, key-management, secret-store, template-store

The second row is this lesson’s decision-carrying finding. Closing public access at the bucket level closes nine thousand two hundred and sixty personal records — fifteen percent of the open records — and stops not a single legitimate job a day. Against that, the spread radius stays at ten. The only item closed is the group that inherits the bucket-level permission; the six permissions explicitly written at the object level stay in place, and the group carrying the identity reference is one of them.

The fourth row shows the reverse. Closing the single open item of type configuration closes forty-seven records — less than a thousandth of the open records — and brings the radius down from ten to five. The cost is nine legitimate accesses a day. The bucket-level fix closes one hundred and ninety-seven times as many records as this fix, and lowers the radius by zero items.

The block below separates out why. The radius has two components: the number of buckets read without an identity, and the number of items added from the identity reference. The first three fixes never touch the second component — the four items stay in place across all three. The fourth fix zeroes out the second component and lowers the first component by only one. The fix that lowers record volume and the fix that lowers the radius are not the same fix.

Each Open Group’s Individual Share

Seven open groups give a combined figure; what each one carries is measured separately.

// cloud/run3.mjs — open groups one by one: how large a radius each carries on its own
import { GROUP, open, radius, walk } from './storage.mjs';

const alone = (g) => {                              // radius if only this group were open
  const y = new Set([g.bucket]);
  if (g.identity) for (const r of walk(g.identity)) y.add(r);
  return y.size;
};
const print = (g, ...s) => console.log(s.map((v, i) =>
  g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i])).join(''));
const A = [-21, -15, 9, 10, 8, 15];
print(A, 'open group alone', 'data type', 'records', 'radius', 'legit', 'records / item');
const openGroup = GROUP.filter((g) => open(g));
for (const g of openGroup) print(A, g.group, g.type, g.records, alone(g), g.legitimate, Math.round(g.records / alone(g)));
const total = openGroup.reduce((a, g) => a + alone(g), 0);
console.log(`\nsum of the individual radii ${total}, radius measured together ` +
  `${radius({}).size}: ${total - radius({}).size} item comes from two groups at once`);
open group alone     data type        records    radius   legit records / item
site-assets          asset               1400         1    8600           1400
upload-preview       subscriber-personal     9260         1       0           9260
daily-upload         measurement        41800         1      40          41800
bulk-export          billing              310         1       0            310
shared-pull          measurement         8700         1       6           8700
old-backup           backup               180         1       0            180
distribution-templateconfiguration         47         5       9              9

sum of the individual radii 11, radius measured together 10: 1 item comes from two groups at once

Six groups have a radius of one: the open bucket is read and that is where it ends. The seventh is five. The last column turns this into a single ratio — records per item. daily-upload carries forty-one thousand eight hundred records per item; distribution-template carries nine. The four-thousand-six-hundred-fold difference explains why the two groups get different treatment on the same list: one is data to be read, the other is a door to be opened.

The last row tests whether the measure is additive. The sum of the individual radii is eleven, the radius measured together is ten. The difference is one item: because two open groups sit in the same bucket, that bucket was counted twice. The spread radius is a set size, and it cannot be summed group by group.

The Separation of the Two Measures

Exposure cannot be given as a single number, because there are two separate forms of harm. Reading personal records is a direct harm, and its size is measured by record count: nine thousand two hundred and sixty subscriber records are read without requiring any walk at all. Reading the configuration file, on the other hand, is small on its own — forty-seven records — but it is a door that opens onto four other items.

The two measures produce different priorities. Sorted by record volume, the first fix is upload-preview; sorted by radius, the first fix is distribution-template. The right order is that both are close to free: the first stops zero legitimate accesses a day, the second stops nine. The fix in the third row — closing items with no legitimate access at all — comes down to four open items and costs zero, but it does not cover the configuration item, because that item gets nine legitimate accesses a day.

The last row draws the boundary. Closing all public access zeroes out the radius; the cost is eight thousand six hundred and fifty-five accesses a day, and eight thousand six hundred of those belong to the subscriber portal’s assets. This is not a storage decision, it is a decision about how the portal works — and that item being open contributes nothing to the radius, because it carries no identity reference.

Summary

  • 7 of 13 object groups are publicly open; the open record count is 61,697, 3.2% of the total. The data type measure is different: 6 of 7 types are open, and the only type left closed is the application log.
  • The effective permission comes from the object level: because one of two groups sitting in the same bucket never wrote its own permission, 9,260 subscriber personal records stay open while getting 0 legitimate accesses a day.
  • The exposure radius has two components: 6 buckets read without an identity and 4 items added from the identity reference in an open configuration file — a total of 10.
  • Closing public access at the bucket level closes 9,260 records, stops 0 jobs a day, and leaves the radius at 10. The fix that lowers record volume may not lower the radius.
  • Closing the 47-record configuration item brings the radius down from 10 to 5 and stops 9 legitimate accesses a day; it closes 197 times fewer records than the bucket-level fix, and lowers the radius by 5 items.
  • Closing all public access zeroes out the radius; the cost is 8,655 accesses a day, and 8,600 of those come from a single item that contributes nothing to the radius.

Next Step

This lesson took the content of an open file to be readable. The records were encrypted on disk, but that was no obstacle for a subject that could read them — because decryption happened on the provider’s side, as part of the read request. The question not asked is this: does the radius change depending on who holds the decryption key? The next lesson runs key ownership through three modes and measures a single thing: the number of items the leaked identity can read, how many when the key is with the provider and how many when it is with the customer.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close