Back to Blog
2026-03-06
5 min read
Install adk-secure-sessions, swap one import, and verify your agent's session data is encrypted at rest — start to finish in under 5 minutes.
In Part 1, I showed that Google ADK stores everything your agent knows — tool calls, user messages, conversation context — in plaintext SQLite. If that made you uncomfortable, this post fixes it.
This is the recipe card. Ingredients, steps, done. The explanation of why the soufflé rises comes later in Part 3.
DatabaseSessionService (or a willingness to create a minimal one)No system libraries, no C compilation, no Docker. The library is pure Python with two runtime dependencies: google-adk and cryptography. A short ingredient list.
pip install adk-secure-sessions
Or with uv:
uv add adk-secure-sessions
Verify the install:
python -c "import adk_secure_sessions; print('OK')"
Your agent code probably has something like this:
# Before — ADK default (unencrypted):
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(
db_url="sqlite+aiosqlite:///sessions.db"
)
Replace it with:
# After — encrypted:
from adk_secure_sessions import EncryptedSessionService, FernetBackend
session_service = EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=FernetBackend("your-secret-passphrase"),
)
Two changes: the import line and the constructor. Everything else in your agent stays the same — create_session, get_session, list_sessions, delete_session, append_event — the full ADK session lifecycle, identical behavior. The difference is what hits the disk.
For proper connection cleanup, wrap the service in async with. Here's a complete, runnable script:
import asyncio
from adk_secure_sessions import EncryptedSessionService, FernetBackend
async def main():
backend = FernetBackend("my-secret-passphrase")
async with EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=backend,
) as service:
# Create a session with sensitive state
session = await service.create_session(
app_name="my-agent",
user_id="user-123",
state={
"patient_name": "Jane Doe",
"diagnosis_code": "J06.9",
"api_key": "sk-secret-key-12345",
},
)
print(f"Created session: {session.id}")
# Retrieve — state is automatically decrypted
session = await service.get_session(
app_name="my-agent",
user_id="user-123",
session_id=session.id,
)
print(f"Decrypted state: {session.state}")
# List sessions for this app/user
response = await service.list_sessions(
app_name="my-agent",
user_id="user-123",
)
print(f"Sessions found: {len(response.sessions)}")
# Clean up when you're done
await service.delete_session(
app_name="my-agent",
user_id="user-123",
session_id=session.id,
)
print("Session deleted")
asyncio.run(main())
Copy this into a file and run it. The API behaves identically to ADK's DatabaseSessionService — same methods, same signatures, same return types. The only difference is what's stored on disk: switching from a glass jar to a lockbox. Same ingredients go in, same ingredients come out, but nobody can peek inside without the key.
Trust but verify. Open the SQLite database directly and confirm the data is actually encrypted.
Using the sqlite3 CLI:
sqlite3 sessions.db "SELECT state FROM sessions LIMIT 1;"
You'll see a base64-encoded string — the encrypted envelope — not readable JSON:
AQFnQUFBQUJuVm1Gc2RX...
Using Python:
import sqlite3
conn = sqlite3.connect("sessions.db")
row = conn.execute("SELECT state FROM sessions LIMIT 1").fetchone()
print(row[0][:60]) # First 60 chars of the encrypted envelope
conn.close()
What you won't see: {"patient_name": "Jane Doe", "diagnosis_code": "J06.9"}. That's the point. With DatabaseSessionService, anyone with file access reads your mise en place. With EncryptedSessionService, they see noise.
For a more convincing demo, run the basic usage example from the repo — it runs a real multi-turn ADK agent with Ollama and then inspects the raw database to prove no plaintext leaks. After a three-turn conversation about patient intake, the database contains zero occurrences of "Jane Doe" or "headache."
The passphrase is the only secret. Never hardcode it.
import os
from adk_secure_sessions import EncryptedSessionService, FernetBackend
backend = FernetBackend(os.environ["SESSION_KEY"])
Set it in your environment, your .env file, or your secrets manager. The library handles everything else — FernetBackend derives a cryptographic key using PBKDF2-HMAC-SHA256 with 480,000 iterations. You don't need to generate, store, or rotate raw key material.
If you use the wrong passphrase to read a session encrypted with a different one, you get a clear DecryptionError — never garbage data, never silent corruption.
Five steps, plaintext to encrypted-at-rest:
pip install adk-secure-sessionsYour agent still works the same way. Your tests still pass. But the SQLite file is now useless without the key — like a walk-in freezer with a combination lock. Nothing changes about how the food is stored or retrieved, but the back door isn't open anymore.
When things go wrong, the library tells you what happened:
ConfigurationError — raised at startup if the backend is misconfigured. You'll catch this before any data is written.DecryptionError — raised if you read a session with the wrong key. The library never returns garbage.from adk_secure_sessions import (
ConfigurationError,
DecryptionError,
EncryptedSessionService,
FernetBackend,
)
try:
async with EncryptedSessionService(
db_url="sqlite+aiosqlite:///sessions.db",
backend=FernetBackend("correct-passphrase"),
) as service:
session = await service.get_session(
app_name="my-agent",
user_id="user-123",
session_id="some-session-id",
)
if session is None:
print("Session not found")
except ConfigurationError:
print("Backend doesn't conform to EncryptionBackend protocol")
except DecryptionError:
print("Wrong key — cannot decrypt session data")
pip install adk-secure-sessions, swap the constructor, donecreate_session, get_session, list_sessions, delete_session, and append_event all work identicallyDecryptionError for wrong keys, ConfigurationError for bad setup