Map every copy of a prompt, then encrypt the hop and the archive

short · 45 min · Objective 2.4

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

  1. Create lab/crypto-map.csv with header item,state,location,carries,protection_before,control,evidence. state is one of transit, rest, in-use or key. 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.
  2. Run ss -ltnp and fill the evidence column for every hop with the address and port actually bound. Search the vector store directory with grep -a -r -c CANARY-7Q4X-SECAI and 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.
  3. Generate a self-signed certificate for the gateway: mkdir -p certs backups, then openssl 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, then chmod 600 certs/gw.key.
  4. Change gateway.py to serve TLS on its existing port, 8088: build an ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER), call load_cert_chain with 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) and server.serve_forever(). That removes the plain listener. Add export GW=https://127.0.0.1:8088, and CURL_CA_BUNDLE and SSL_CERT_FILE both set to ~/secai-lab/certs/gw.crt, to your ~/.bashrc as well as the current shell: every later lab, and app/ask.py, reaches the gateway through them.
  5. 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 control what production would need instead: mutual TLS, usually from a service mesh, so the runner refuses every caller but the gateway.
  6. 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 with gpg --batch --pinentry-mode loopback --passphrase-file ~/.secai-archive-pass --symmetric --cipher-algo AES256 -o backups/secai.tar.gpg backups/secai.tar and delete the plain tar. The passphrase file lives outside ~/secai-lab with mode 600: record that in a key row, because a key stored beside its data protects nothing.
  7. Add a row for the runner's memory with control set to what would protect it (a trusted execution environment with attestation-gated key release) and protection_before set to none in this lab.
  8. 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 item set to legitimate query path and a control naming 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.