Cion-lab commited on
Commit
0e945ba
·
verified ·
1 Parent(s): a4b475b

p1 bootstrap v2: licenses key required; existence != privacy; stage in /tmp

Browse files
Files changed (1) hide show
  1. probes/p1_secret_bootstrap.py +83 -65
probes/p1_secret_bootstrap.py CHANGED
@@ -1,23 +1,23 @@
1
  # Phase 1 bootstrap worker: create a PRIVATE Kaggle dataset to hold the HF token, safely.
2
  #
3
- # Why this exists (closes D-002). §2 permits the token to come "from the environment or a secret
4
- # store" and forbids it reaching a public artifact. p1_credential_path proved the Kaggle CLI is
5
- # preinstalled and already authenticated inside an internet-enabled session, so a job can create its
6
- # own private dataset and later jobs can mount it from /kaggle/input instead of carrying the token in
7
- # their source.
8
  #
9
- # THE ORDERING HERE IS A SECURITY CONTROL, NOT STYLE. `kaggle datasets create` decides visibility
10
- # from a metadata field whose name has varied between CLI versions (isPrivate vs defineIsPrivate), and
11
- # an unrecognised key is silently ignored -- which would create a PUBLIC dataset. A public dataset
12
- # containing the token would be an unrecoverable leak, so:
 
 
 
13
  #
14
- # 1. create with an inert placeholder file only,
15
- # 2. PROVE it is private by failing to download that file anonymously,
16
- # 3. only then push the real credential as a new version,
17
- # 4. re-prove, and delete nothing that was not created here.
18
  #
19
- # If step 2 ever shows anonymous read succeeding, the secret is never uploaded and the job says so.
20
- # No token value is ever printed; kernel stdout is retained. Zero GPU quota (CPU session).
21
 
22
  import hashlib
23
  import json
@@ -31,7 +31,7 @@ R = {}
31
  DS_SLUG = "ounce100m-secret-store"
32
  DS_ID = f"dodosoomro/{DS_SLUG}"
33
  TOKEN_ENV = "OUNCE100M_HF_TOKEN"
34
- DS_DIR = "/kaggle/working/ds"
35
  PLACEHOLDER = "placeholder.txt"
36
  SECRET_FILE = "credentials.json"
37
 
@@ -49,44 +49,54 @@ def run(cmd, timeout=300):
49
  return {"rc": p.returncode, "out": scrub(p.stdout), "err": scrub(p.stderr)}
50
 
51
 
52
- def anon_can_read(path):
53
- """True only if an unauthenticated request can fetch the file. That means PUBLIC -- bad."""
 
 
 
 
 
 
 
54
  url = f"https://www.kaggle.com/api/v1/datasets/download/{DS_ID}"
55
  try:
56
  with urllib.request.urlopen(url, timeout=60) as r:
57
- return {"readable": True, "status": r.status, "bytes": len(r.read(2048))}
58
  except urllib.error.HTTPError as e:
59
- return {"readable": False, "status": e.code, "reason": str(e.reason)[:80]}
 
60
  except Exception as e:
61
- return {"readable": None, "error": f"{type(e).__name__}: {e}"[:160]}
62
 
63
 
64
- def write_metadata(private=True):
65
- """Both spellings of the visibility key, because the CLI version in the image is 2.0.2 and the
66
- field name has moved across releases; an unrecognised key is ignored without error."""
67
- md = {
68
- "title": "ounce100m runtime assets",
69
- "id": DS_ID,
70
- "resources": [{"path": PLACEHOLDER, "description": "bootstrap placeholder",
71
- "resourceName": "placeholder", "resourceType": "dataset"}],
72
- }
73
- if private:
74
- md["isPrivate"] = True
75
- md["defineIsPrivate"] = True
76
- with open(os.path.join(DS_DIR, "dataset-metadata.json"), "w") as f:
77
- json.dump(md, f)
78
-
79
-
80
- def write_files(with_secret):
81
  os.makedirs(DS_DIR, exist_ok=True)
82
  with open(os.path.join(DS_DIR, PLACEHOLDER), "w") as f:
83
  f.write("ounce100m bootstrap placeholder\n")
84
  if with_secret:
85
- tok = os.environ[TOKEN_ENV]
86
  with open(os.path.join(DS_DIR, SECRET_FILE), "w") as f:
87
- json.dump({"HF_TOKEN": tok}, f)
88
  os.chmod(os.path.join(DS_DIR, SECRET_FILE), 0o600)
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  tok = os.environ.get(TOKEN_ENV, "")
92
  R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
@@ -95,44 +105,52 @@ R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
95
  if not tok:
96
  R["fatal"] = f"{TOKEN_ENV} unset; nothing created"
97
  else:
98
- write_files(with_secret=False)
99
- write_metadata(private=True)
100
-
101
- existing = run(f"kaggle datasets status {DS_ID} 2>&1", 120)
102
- R["pre_existing_status"] = existing
103
- already = existing["rc"] == 0
104
-
105
- if not already:
106
  t = time.monotonic()
107
  R["create"] = run(f"kaggle datasets create -p {DS_DIR} -q 2>&1", timeout=600)
108
  R["create"]["seconds"] = round(time.monotonic() - t, 1)
109
- time.sleep(12) # visibility is applied asynchronously; an immediate probe can lie
110
 
111
- # ---- the gate: prove privacy BEFORE any secret exists on the dataset
112
- privacy = anon_can_read(PLACEHOLDER)
113
- R["privacy_check_placeholder_only"] = privacy
114
- R["private_confirmed"] = privacy.get("readable") is False
 
 
115
 
116
- if R["private_confirmed"]:
117
- write_files(with_secret=True)
118
  t = time.monotonic()
119
  R["version_with_secret"] = run(
120
  f'kaggle datasets version -p {DS_DIR} -m "provision runtime credential" -q 2>&1',
121
  timeout=600)
122
  R["version_with_secret"]["seconds"] = round(time.monotonic() - t, 1)
123
- time.sleep(12)
124
- after = anon_can_read(SECRET_FILE)
125
- R["privacy_check_after_secret"] = after
126
- R["secret_still_private"] = after.get("readable") is False
127
  if not R["secret_still_private"]:
128
- R["EMERGENCY"] = ("Anonymous read succeeded AFTER the credential was uploaded -- the "
129
- "dataset may be public. Delete dodosoomro/ounce100m-secret-store from "
130
- "the Kaggle web UI immediately and rotate the HF token.")
 
 
 
 
131
  else:
132
- R["refused_to_upload"] = ("Dataset was anonymously readable while holding only a placeholder; "
133
- "the secret was NOT uploaded. Fix visibility handling before retry.")
 
 
 
134
 
135
- R["files"] = run(f"kaggle datasets files {DS_ID} 2>&1", 120)
 
 
 
 
 
136
  if R.get("secret_still_private"):
137
  R["mount_hint_for_future_jobs"] = {
138
  "datasetDataSources": [DS_ID],
 
1
  # Phase 1 bootstrap worker: create a PRIVATE Kaggle dataset to hold the HF token, safely.
2
  #
3
+ # Why (closes D-002): §2 permits "the environment or a secret store" and forbids the token reaching a
4
+ # public artifact. p1_credential_path showed the Kaggle CLI is preinstalled and pre-authenticated
5
+ # inside an internet-enabled session, so a job can create its own private dataset and later jobs can
6
+ # mount it from /kaggle/input instead of carrying the token in their source.
 
7
  #
8
+ # v1 FAILED, and the failure taught two things that are encoded here:
9
+ # * `kaggle datasets create` on CLI 2.0.2 aborts with "Key licenses not found in data" unless
10
+ # dataset-metadata.json carries a `licenses` array. Nothing was created, so nothing leaked.
11
+ # * worse, my privacy gate was NOT discriminating: it treated an anonymous 403 as "private", but a
12
+ # 403 is also what a NON-EXISTENT dataset returns. So v1 reported private_confirmed=true for a
13
+ # dataset that did not exist. Existence and privacy are now proved by two different calls, and
14
+ # privacy is only asserted when the dataset is known to exist.
15
  #
16
+ # Staging lives under /tmp, never /kaggle/working: files in the working directory are published as
17
+ # kernel output artifacts, and v1 briefly wrote the credential there. It stayed behind private-kernel
18
+ # auth, but there is no reason for a secret to be an output artifact at all.
 
19
  #
20
+ # No token value is ever printed. Zero GPU quota (CPU session).
 
21
 
22
  import hashlib
23
  import json
 
31
  DS_SLUG = "ounce100m-secret-store"
32
  DS_ID = f"dodosoomro/{DS_SLUG}"
33
  TOKEN_ENV = "OUNCE100M_HF_TOKEN"
34
+ DS_DIR = "/tmp/ounce100m_ds" # outside /kaggle/working on purpose -- see header
35
  PLACEHOLDER = "placeholder.txt"
36
  SECRET_FILE = "credentials.json"
37
 
 
49
  return {"rc": p.returncode, "out": scrub(p.stdout), "err": scrub(p.stderr)}
50
 
51
 
52
+ def exists_authenticated():
53
+ """A successful *authenticated* listing proves the dataset exists."""
54
+ r = run(f"kaggle datasets files {DS_ID} 2>&1", 120)
55
+ return {"rc": r["rc"], "out": r["out"][:400], "exists": r["rc"] == 0}
56
+
57
+
58
+ def anon_access():
59
+ """Classify anonymous reachability. Only HTTP 200 means PUBLIC-and-leaking. 404 means the dataset
60
+ is not there at all, which is NOT evidence of privacy."""
61
  url = f"https://www.kaggle.com/api/v1/datasets/download/{DS_ID}"
62
  try:
63
  with urllib.request.urlopen(url, timeout=60) as r:
64
+ return {"status": r.status, "verdict": "PUBLIC"}
65
  except urllib.error.HTTPError as e:
66
+ return {"status": e.code,
67
+ "verdict": "PRIVATE" if e.code in (401, 403) else "OTHER_HTTP"}
68
  except Exception as e:
69
+ return {"error": f"{type(e).__name__}: {e}"[:160], "verdict": "UNKNOWN"}
70
 
71
 
72
+ def write_metadata(with_secret):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  os.makedirs(DS_DIR, exist_ok=True)
74
  with open(os.path.join(DS_DIR, PLACEHOLDER), "w") as f:
75
  f.write("ounce100m bootstrap placeholder\n")
76
  if with_secret:
 
77
  with open(os.path.join(DS_DIR, SECRET_FILE), "w") as f:
78
+ json.dump({"HF_TOKEN": os.environ[TOKEN_ENV]}, f)
79
  os.chmod(os.path.join(DS_DIR, SECRET_FILE), 0o600)
80
 
81
+ resources = [{"path": PLACEHOLDER, "description": "bootstrap placeholder",
82
+ "resourceName": "placeholder", "resourceType": "dataset"}]
83
+ if with_secret:
84
+ resources.append({"path": SECRET_FILE, "description": "runtime credentials",
85
+ "resourceName": "credentials", "resourceType": "dataset"})
86
+ md = {
87
+ "title": "ounce100m runtime assets",
88
+ "id": DS_ID,
89
+ # CLI 2.0.2 refuses to create without this key -- the exact fault that stopped v1.
90
+ "licenses": [{"name": "other"}],
91
+ "resources": resources,
92
+ # Both historical spellings of the visibility flag: an unrecognised key is ignored silently,
93
+ # and a silently-ignored key here would mean a public dataset holding a credential.
94
+ "isPrivate": True,
95
+ "defineIsPrivate": True,
96
+ }
97
+ with open(os.path.join(DS_DIR, "dataset-metadata.json"), "w") as f:
98
+ json.dump(md, f)
99
+
100
 
101
  tok = os.environ.get(TOKEN_ENV, "")
102
  R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
 
105
  if not tok:
106
  R["fatal"] = f"{TOKEN_ENV} unset; nothing created"
107
  else:
108
+ write_metadata(with_secret=False)
109
+ if not exists_authenticated()["exists"]:
 
 
 
 
 
 
110
  t = time.monotonic()
111
  R["create"] = run(f"kaggle datasets create -p {DS_DIR} -q 2>&1", timeout=600)
112
  R["create"]["seconds"] = round(time.monotonic() - t, 1)
113
+ time.sleep(15)
114
 
115
+ # Gate 1: does it exist? (authenticated) Gate 2: is it unreachable anonymously?
116
+ ex = exists_authenticated()
117
+ R["existence"] = ex
118
+ priv = anon_access() if ex["exists"] else {"verdict": "SKIPPED_NOT_EXISTING"}
119
+ R["privacy_placeholder_only"] = priv
120
+ R["safe_to_upload_secret"] = bool(ex["exists"]) and priv["verdict"] == "PRIVATE"
121
 
122
+ if R["safe_to_upload_secret"]:
123
+ write_metadata(with_secret=True)
124
  t = time.monotonic()
125
  R["version_with_secret"] = run(
126
  f'kaggle datasets version -p {DS_DIR} -m "provision runtime credential" -q 2>&1',
127
  timeout=600)
128
  R["version_with_secret"]["seconds"] = round(time.monotonic() - t, 1)
129
+ time.sleep(15)
130
+ after = anon_access()
131
+ R["privacy_after_secret"] = after
132
+ R["secret_still_private"] = after["verdict"] == "PRIVATE"
133
  if not R["secret_still_private"]:
134
+ R["EMERGENCY"] = ("Anonymous read works AFTER the credential upload: delete "
135
+ f"{DS_ID} from the Kaggle web UI now and rotate the HF token.")
136
+ # A public dataset would also show up in the public search index -- independent second check.
137
+ R["public_search_check"] = scrub(
138
+ urllib.request.urlopen(
139
+ f"https://www.kaggle.com/api/v1/datasets/list?public=true&user=dodosoomro"
140
+ f"&search=ounce100m", timeout=60).read().decode()[:400])
141
  else:
142
+ R["refused_to_upload"] = {
143
+ "reason": "existence or privacy not positively established",
144
+ "existence_rc": ex["rc"], "privacy": priv,
145
+ "create_output": R.get("create", {}).get("out"),
146
+ }
147
 
148
+ # Remove the staged credential from disk regardless of outcome, then report what is left.
149
+ if os.path.exists(os.path.join(DS_DIR, SECRET_FILE)):
150
+ os.remove(os.path.join(DS_DIR, SECRET_FILE))
151
+ R["staging_after"] = {"secret_removed": not os.path.exists(os.path.join(DS_DIR, SECRET_FILE)),
152
+ "dir_listing": sorted(os.listdir(DS_DIR)),
153
+ "working_dir_listing": sorted(os.listdir("/kaggle/working"))}
154
  if R.get("secret_still_private"):
155
  R["mount_hint_for_future_jobs"] = {
156
  "datasetDataSources": [DS_ID],