Volume 1 covered the foundation - Ubuntu Server, Ollama, Nginx reverse proxy with AD CS TLS certificates, and container hardening. By the end you had a running, network-hardened AI infrastructure. Volume 2 picks up where that left off. A secured deployment is not the same as a secured system. Once your stack is live, a new set of risks emerges - malicious documents entering your RAG pipeline, prompt injection from users, secrets sitting unencrypted in .env files, and no visibility into what the system is actually doing.
All commands can be copied from here.
Volume 2: Enterprise AI & LLM Security Hardening - Runtime Defense & Governance
*
Volume 1 covered the foundation - Ubuntu Server, Ollama, Nginx reverse proxy with AD CS TLS certificates, and container hardening. By the end you had a running, network-hardened AI infrastructure.
Volume 2 picks up where that left off. A secured deployment is not the same as a secured system. Once your stack is live, a new set of risks emerges - malicious documents entering your RAG pipeline, prompt injection from users, secrets sitting unencrypted in .env files, and no visibility into what the system is actually doing.
This volume addresses all of it: secure RAG ingestion, Qdrant vector database hardening, prompt injection defense, a full Prometheus/Grafana/Loki monitoring stack, HashiCorp Vault secrets management, a structured red team exercise, and a compliance mapping to OWASP LLM Top 10 and NIST AI RMF.
Every step was built and verified in a live VMware lab - Ubuntu Server 24.04 joined to a Windows Server 2025 Active Directory domain. Nothing here is theoretical.
*
Task Details
1. Vector Database Hardening - Qdrant - deploy Qdrant from scratch with a generated API key, TLS cert, Docker hardening, and daily cron backups
2. RAG Pipeline Security - install ClamAV, build the ingestion gate with 4 validation checks, write the injection-sanitizing chunker, and wire up safe retrieval with score thresholds
3. Prompt Injection Defense - build the full defense.py layer (blocklist + unicode normalization + PII redaction + output scanner) and the hardened system prompt
4. AI Monitoring & Logging - stand up the Prometheus/Grafana/Loki stack, build the AI logging proxy, and write the 4 alert rules
5. GPU Protection - PowerShell GPU monitor running as a Windows Service, model checksum baseline and daily integrity check
6. Secrets Management - deploy Vault, initialize and unseal it, migrate all secrets, create least-privilege policies, and add Gitleaks to Git
7. Red Teaming Your AI Stack - 7-step exercise with actual commands against your own stack
8. Governance & Compliance - complete OWASP LLM Top 10 and NIST AI RMF mapping, quarterly review checklist
*
All commands can be copied from here.
Steps
Vector Database Hardening - Qdrant
What is a vector database and why does it need hardening?
A vector database stores document embeddings, arrays of floating-point numbers that represent the semantic meaning of text. When your RAG pipeline embeds a document chunk, the resulting vector is stored here. When a user asks a question, that query is also embedded and the database finds the stored vectors that are mathematically closest to it. Those closest vectors represent the most semantically similar documents.
Qdrant is the vector database you'll use. Out of the box it has no authentication and listens on all network interfaces. Anyone who can reach port 6333 can read your entire knowledge base, inject fake documents, or delete everything. This chapter locks it down completely.
*
1. Create the directory structure
Qdrant needs dedicated directories for its data, config, TLS certificates, and snapshots. The storage and snapshots directories need to be writable by UID 1000 - the user Qdrant runs as inside the container.
- mkdir -p ~/qdrant/{config,storage,snapshots,tls}
- sudo chown -R 1000:1000 ~/qdrant/storage ~/qdrant/snapshots
*
2. Generate a strong API key
The API key is what Qdrant requires on every request. Anyone without it gets a 403. Use openssl to generate 32 random bytes encoded as hex - that's 256 bits of entropy. Never create API keys manually or use dictionary words.
Generates a 64-character hex string (32 bytes = 256 bits)
- openssl rand -hex 32
*
Store it in .env - Docker Compose reads this file automatically
- cat > ~/qdrant/.env << 'EOF'
QDRANT_API_KEY=4a9XXXXXXXXXXXXXXXXXXXXXXXX1a7d524d
EOF
*
Make it readable only by your user
- chmod 600 ~/qdrant/.env
*
Prevent it from ever being committed to Git
- echo '.env' >> ~/qdrant/.gitignore
Note: Add .env to .gitignore to prevent sensitive information (API keys, passwords, tokens, and credentials) from being accidentally committed to Git repositories. This follows security best practices and reduces the risk of credential exposure. The .gitignore file instructs Git to ignore .env files during commits.
*
3. Generate a TLS certificate
TLS encrypts traffic between your RAG application and Qdrant. Without it, anyone who can observe traffic on the server can read the vectors and document content being exchanged. Even on localhost this is good practice - it prepares you for configurations where the services run on separate hosts.
If you set up the internal CA in Volume 1 with AD CS, request a cert from there. Otherwise generate a self-signed one:
Note: I set up the antusnet.ca domain in Volume 1, so I will use it throughout this guide.
cd ~/qdrant/tls
*
openssl req -x509 -newkey rsa:4096 \
-keyout key.pem \
-out cert.pem \
-days 365 \
-nodes \
-subj '/CN=qdrant.antusnet.local/O=antusnet/C=CA' \
-addext 'subjectAltName=DNS:qdrant.antusnet.local,IP:127.0.0.1'
Note: "-nodes" means the key has no passphrase, which is acceptable because the key file itself is protected by chmod 600 and only Qdrant reads it.
*
Private key: owner-only
- chmod 600 key.pem
Certificate: readable by others (it's public)
- chmod 644 cert.pem
Verify it was created correctly
- openssl x509 -in cert.pem -text -noout | grep -A2 'Subject\|Validity'
*
*
4. Create the Docker Compose file
- cd ~/qdrant/docker-compose.yml
- sudo nano docker-compose.yml
*
Paste the following code into the docker-compose.yml file and save
This ties everything together. Read each setting - every security comment explains the attack it prevents
services:
qdrant:
image: qdrant/qdrant:v1.9.0
container_name: qdrant
restart: unless-stopped
ports:
- '127.0.0.1:6333:6333'
- '127.0.0.1:6334:6334'
volumes:
- ./config/config.yaml:/qdrant/config/production.yaml:ro
- ./storage:/qdrant/storage
- ./snapshots:/qdrant/snapshots
- ./tls:/qdrant/tls:ro
environment:
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY}
- QDRANT__SERVICE__ENABLE_CORS=false
# - QDRANT__TLS__ENABLE=true
# - QDRANT__TLS__CERT=/qdrant/tls/cert.pem
# - QDRANT__TLS__KEY=/qdrant/tls/key.pem
mem_limit: 4g
cpus: 2.0
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
user: '1000:1000'
*
5. Create ~/qdrant/config/config.yaml file
Remove whatever is there (file or directory)
- rm -rf ~/qdrant/config/config.yaml
Recreate it as an actual file
- touch ~/qdrant/config/config.yaml
Confirm it's a file, not a directory
- ls -la ~/qdrant/config/
*
6. Start Qdrant and verify
- cd ~/qdrant
- docker compose up -d
*
7. Verify the Qdrant logs
- sleep 5
- docker logs qdrant --tail 20
Note: One thing to fix: WARN Failed to create init file indicator: Permission denied (os error 13)
*
8. The storage directory has a permissions issue. Fix it.
- sudo chown -R 1000:1000 ~/qdrant/storage ~/qdrant/snapshots
- docker compose restart qdrant
*
9. Then verify auth works correctly both ways
Should return 401 (no key)
- curl -s -w '%{http_code}' http://localhost:6333/collections -o /dev/null
Should return 200 (with key)
- curl -s -w '%{http_code}' http://localhost:6333/collections \
-H "api-key: $(grep QDRANT_API_KEY ~/qdrant/.env | cut -d= -f2)" \
-o /dev/null
Note: Once you get 200 on the second call, Qdrant is fully hardened and ready for the RAG pipeline integration.
*
9. Automated daily backups
Qdrant's data lives on disk in the storage directory, but if that volume is lost the knowledge base is gone. Qdrant has a built-in snapshot API that creates a consistent point-in-time backup of a collection. Set up a cron job to take one every night and rotate old ones automatically.
Create ~/qdrant/backup.sh file with the following content.
*
#!/bin/bash
set -euo pipefail
COLLECTION='enterprise-kb'
KEY=$(grep QDRANT_API_KEY ~/qdrant/.env | cut -d= -f2)
SNAP_DIR=~/qdrant/snapshots
KEEP_LAST=7
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Starting snapshot..."
# Call the Qdrant snapshot API:
curl -s -X POST "http://localhost:6333/collections/${COLLECTION}/snapshots" \
-H "api-key: ${KEY}"
# Rotate — delete all but the most recent KEEP_LAST snapshots.
# ls -t sorts newest-first. tail -n +N outputs from line N onwards.
ls -t "${SNAP_DIR}/${COLLECTION}"/*.snapshot 2>/dev/null \
| tail -n +$((KEEP_LAST + 1)) \
| xargs -r rm -v
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Done"
chmod +x ~/qdrant/backup.sh
# Always test manually before trusting a cron job:
~/qdrant/backup.sh
# Add to cron (2am daily)
crontab -e
# Add this line:
# 0 2 * * * /home/ubuntu/qdrant/backup.sh >> /var/log/qdrant-backup.log 2>&1
*
RAG Pipeline Security
All commands can be copied from here.
*
What is a RAG pipeline and why does it need securing?
Retrieval-Augmented Generation (RAG) connects your LLM to internal documents. Instead of retraining the model, you store document chunks in a vector database. When a user asks a question, the system retrieves the most relevant chunks and passes them to the LLM as context. The LLM answers using that context instead of guessing from training data.
The security problem: every document you ingest is a potential attack vector. A malicious file can execute code during processing. A document with hidden instructions can hijack the model's responses. The pipeline has three distinct threat zones — ingestion, retrieval, and generation and each needs its own controls.
Create your first collection
1. Create your first collection
A collection in Qdrant is equivalent to a table in a relational database. It holds vectors of a fixed dimension with associated metadata. The dimension must exactly match the output size of your embedding model. Mismatches cause silent errors.
- mkdir -p ~/rag-secure
- cd ~/rag-secure
*
2. Create a virtual environment
You do this to create a clean, isolated Python environment for your project instead of installing packages globally on your system.
It like each project gets its own mini Python computer.
- python3 -m venv venv
- source venv/bin/activate
- pip install "qdrant-client==1.9.0"
Note: 1.9.0 - replace with your version
--SNIP--
*
3. Create the vector_store.py file
- nano ~/rag-secure/vector_store.py
--SNIP--
*
4. Run the test
- export QDRANT_API_KEY=$(grep QDRANT_API_KEY ~/qdrant/.env | cut -d= -f2)
- python3 vector_store.py
Note: Test OK: 1 point inserted, collection count: 2 - the run found the existing collection (skipped creation) and added a second test point.
That's correct behavior. The insecure connection warning is expected until TLS is enabled later.
Note: The “export QDRANT_API_KEY...” command automatically loads your Qdrant API key from ~/qdrant/.env into the current terminal session so other commands can authenticate to Qdrant without hardcoding the key.
























































