Map every copy of a prompt, then encrypt the hop and the archive
Task
Trace one question through your toy RAG app and record every place its content exists -- in transit, at rest and in use -- then put TLS on the gateway and encrypt the archive copy of the logs and the vector store. Finish by proving the trap the lesson describes: neither control changes what an authorised query can read.
Steps
- Create
lab/crypto-map.csvwith headeritem,state,location,carries,protection_before,control,evidence.stateis one oftransit,rest,in-useorkey. Add one row per hop (client to gateway, gateway to runner, app to embedding model, app to vector store) and one per store (docs/, the vector store directory, the model weights file, logs/gateway.jsonl, any conversation history or cache) plus one for the runner's memory during inference. - Run
ss -ltnpand fill theevidencecolumn for every hop with the address and port actually bound. Search the vector store directory withgrep -a -r -c CANARY-7Q4X-SECAIand record the result: most stores keep chunk text beside the vectors, which is why the lesson calls the vector store the copy most often missed. - Generate a self-signed certificate for the gateway:
mkdir -p certs backups, thenopenssl req -x509 -newkey rsa:2048 -nodes -keyout certs/gw.key -out certs/gw.crt -days 30 -subj /CN=localhost -addext subjectAltName=IP:127.0.0.1,DNS:localhost, thenchmod 600 certs/gw.key. - Change gateway.py to serve TLS on its existing port, 8088: build an
ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER), callload_cert_chainwith the two files, and replace the last line with three:server = ThreadingHTTPServer(("127.0.0.1", 8088), Gateway),server.socket = ctx.wrap_socket(server.socket, server_side=True)andserver.serve_forever(). That removes the plain listener. Addexport GW=https://127.0.0.1:8088, andCURL_CA_BUNDLEandSSL_CERT_FILEboth set to~/secai-lab/certs/gw.crt, to your~/.bashrcas well as the current shell: every later lab, andapp/ask.py, reaches the gateway through them. - Confirm the runner is still bound to loopback only. Record the gateway-to-runner hop as an accepted residual for a one-host lab, and write in
controlwhat production would need instead: mutual TLS, usually from a service mesh, so the runner refuses every caller but the gateway. - Build the archive copy uncompressed, so the test can see inside it:
tar -cf backups/secai.tar logs <your vector store directory>. Count the canary in it (it will be there). Create the passphrase file with(umask 077; python -c "import secrets; print(secrets.token_urlsafe(32))" > ~/.secai-archive-pass), then encrypt the archive withgpg --batch --pinentry-mode loopback --passphrase-file ~/.secai-archive-pass --symmetric --cipher-algo AES256 -o backups/secai.tar.gpg backups/secai.tarand delete the plain tar. The passphrase file lives outside ~/secai-lab with mode 600: record that in akeyrow, because a key stored beside its data protects nothing. - Add a row for the runner's memory with
controlset to what would protect it (a trusted execution environment with attestation-gated key release) andprotection_beforeset tonone in this lab. - Ask, as carol (or as any user if your lesson 5 retrieval filter is not in place), a question that retrieves salaries.md, over the new TLS endpoint. The canary comes back. Add a row with
itemset tolegitimate query pathand acontrolnaming access control at retrieval, not encryption.
Verify
curl -s -o /dev/null -w 'plain http to the TLS port: %{http_code}' http://127.0.0.1:8088/ask; echo
curl -s "$GW/ask" -H 'Content-Type: application/json' -d '{"user":"alice","question":"How many days of annual leave do staff get?"}' | python -c "import json,sys; print('tls:', 'ok' if json.load(sys.stdin).get('response') else 'FAIL')"
ss -ltn | awk -v p=":$RUNNER_PORT" '$4 ~ (p "$") && $4 !~ /^(127[.]0[.]0[.]1|[[]::1[]]):/ {n++} END {print n+0, "runner listener(s) off loopback"}'
grep -a -c CANARY-7Q4X-SECAI backups/secai.tar.gpg
gpg --batch --pinentry-mode loopback --passphrase-file ~/.secai-archive-pass -d backups/secai.tar.gpg 2>/dev/null | grep -a -c CANARY-7Q4X-SECAI
awk -F, 'NR>1 {print $2}' lab/crypto-map.csv | sort | uniq -c
grep -c 'legitimate query path' lab/crypto-map.csv
Plain HTTP to the TLS port returns 000, the TLS request prints tls: ok, zero runner listeners sit off loopback, the encrypted archive holds the canary ZERO times while the decrypted stream holds it at least once, and the map shows all four states with the legitimate-query-path row present. A zero on the decrypted stream means the archive never contained the store, so the encrypted zero proved nothing.
Notes
Gzip would also hide the canary from grep, which is why the archive is built uncompressed before encryption: compression is not confidentiality, and a test that cannot tell them apart is not a test. Keep the certificate and CURL_CA_BUNDLE setting; every later lab in this domain talks to the gateway through them.
This is an independent study companion for CompTIA SecAI+ CY0-001 and is not produced by or endorsed by CompTIA.