SessionArmor
Framework-neutral session-assurance contracts with an optional Django adapter for session integrity, privacy-aware audit logging, and configurable request gates.
Part of the Tunet ecosystem — alongside SwapLayer and InfraGlyph. See the SessionArmor product page for the public capability overview.
What It Does
The dependency-free core provides immutable assurance evidence and typed gate decisions. The optional Django adapter provides three drop-in middleware classes that harden an application's session layer:
| Middleware | Purpose |
|---|---|
SessionSecurityMiddleware |
Keyed continuity binding, validated absolute/idle timeouts, claim drift detection, and typed assurance evidence |
AuditMiddleware |
Structured, privacy-aware security audit events for authenticated request outcomes |
GateMiddleware |
Typed, identity-bound workflow gates with bounded, revocation-aware caching |
All three are hookable — override methods to customize behavior without touching internals.
References
- NIST SP 800-53 AC-12 (Session Termination)
- NIST SP 800-53 SC-23 (Session Authenticity)
- NIST SP 800-53 AU-3/AU-12 (Audit Content/Generation)
- OWASP Session Management Cheat Sheet
SessionArmor adds controls around an existing Django authentication and session setup; it does not replace HTTPS, secure cookie settings, CSRF protection, authorization, MFA, or incident response. Fingerprint binding is a replay signal rather than proof of device identity and can require tuning for mobile or privacy-network traffic. See the security model for deployment assumptions, limitations, and a release checklist.
Installation
pip install SessionArmor
The core install has no framework dependency. Install the Django adapter when middleware integration is required:
pip install "SessionArmor[django]"
Quick Start
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# ... your auth middleware ...
'session_armor.adapters.django.SessionSecurityMiddleware',
'session_armor.adapters.django.AuditMiddleware',
# ... rest of your middleware ...
]
# Optional settings (shown with defaults)
SESSION_ARMOR_ABSOLUTE_TIMEOUT = 28800
SESSION_ARMOR_IDLE_TIMEOUT = 1800
SESSION_ARMOR_BIND_IP = True
SESSION_ARMOR_BIND_USER_AGENT = True
SESSION_ARMOR_DETECT_CLAIM_DRIFT = True
SESSION_ARMOR_REQUIRE_BINDING = True
SESSION_ARMOR_CLOCK_SKEW = 60
SESSION_ARMOR_LAST_ACTIVE_RESOLUTION = 0
SESSION_ARMOR_NO_STORE = True
SESSION_ARMOR_STATE_KEY = SECRET_KEY
SESSION_ARMOR_AUDIT_PEPPER = SECRET_KEY
# Forwarded headers are ignored unless both values are configured.
SESSION_ARMOR_TRUSTED_PROXY_DEPTH = 0
SESSION_ARMOR_TRUSTED_PROXY_CIDRS = ()
Note on audit IP hashing. Audit logs pseudonymize the client IP with a keyed HMAC-SHA256 so the value can't be reversed from the (low-entropy) IPv4 space. Set a dedicated
SESSION_ARMOR_AUDIT_PEPPERto rotate it independently ofSECRET_KEY, or to share it across services for cross-log correlation. If unset, it falls back toSECRET_KEY.
Note on proxy trust. The secure default (
DEPTH = 0) ignoresX-Forwarded-For. To use it, configure both the exact number of proxy hops and their CIDR ranges. Each trusted hop is checked from the socket peer back toward the client; malformed chains and catch-all trusted networks are rejected.
Building a Gate
from session_armor import GateDecision
from session_armor.adapters.django import GateMiddleware
from django.shortcuts import redirect
class ComplianceGate(GateMiddleware):
gate_id = 'compliance'
cache_ttl_setting = 'COMPLIANCE_CACHE_TTL'
default_cache_ttl = 3600
requires_trusted_session = True
def check(self, request) -> GateDecision:
if user_accepted_current_terms(request):
return GateDecision.allow('current_terms_accepted')
return GateDecision.reject('terms_acceptance_required')
def on_reject(self, request, decision):
return redirect('/accept-terms/')
"Check once" gates
For gates that should only kick in once (onboarding, compliance), set
recheck_after_pass = False: a passing result stays cached in the session
with no TTL re-checks. To push new requirements to already-passed sessions
(a new document version, a new onboarding step), implement
get_state_version() against a shared cache key and bump that key on
publish — every active session re-checks exactly once, with no per-request
database hits:
from django.core.cache import cache
class ComplianceGate(GateMiddleware):
gate_id = 'compliance'
recheck_after_pass = False # Pass once, stay passed
def get_state_version(self, request) -> str | None:
# Bumped by admin tooling when a new document version is published
return cache.get(f'compliance_version:{request.user.platform}', '')
def check(self, request) -> GateDecision:
return (
GateDecision.allow('current_terms_accepted')
if user_accepted_current_terms(request)
else GateDecision.reject('terms_acceptance_required')
)
Sticky passes and revocation. With
recheck_after_pass = False, a pass is cached until the session ends,invalidate()is called, or the state version changes — so it is not appropriate for authorization/entitlement gates where access may be revoked. For those, use a normal TTL gate, driveget_state_version(), or setsticky_pass_max_age(seconds) to cap how long a sticky pass may be reused before a forced re-check.
Customizing Session Security
from session_armor.adapters.django import SessionSecurityMiddleware
class MySessionSecurity(SessionSecurityMiddleware):
exempt_paths = ('/health/', '/static/')
last_active_resolution = 60 # only rewrite last-active timestamp every 60s
def get_critical_claims(self, user):
# Auth0 / OIDC claims that must not change mid-session
return [user.sub, user.organization_uuid, user.platform]
def get_login_url(self, request):
platform = getattr(request.user, 'platform', '')
return f'/{platform}/login/' if platform else '/login/'
# After a reviewed server-side claim update, use the public API rather than
# writing SessionArmor's private session keys.
MySessionSecurity(lambda request: request).rebaseline_claims(request)
AccessGate integration
SessionArmor and AccessGate are complementary boundaries. SessionArmor decides
whether the authenticated session is still trustworthy and attaches an immutable
request.session_assurance record. AccessGate's Django adapter carries that
record into authorization context, where SessionAssurancePolicy denies stale,
missing, malformed, or subject-mismatched evidence before business policies may
grant an action:
from access_gate import AuthorizationEngine, RolePolicy, SessionAssurancePolicy
engine = AuthorizationEngine([
SessionAssurancePolicy(actions={'records.read'}),
RolePolicy({'records_reader'}, actions={'records.read'}),
])
The assurance policy is deny-only: a trusted session never grants a permission
by itself. See docs/ACCESSGATE_INTEGRATION.md.
Customizing Audit Logging
from session_armor.adapters.django import AuditMiddleware
class MyAudit(AuditMiddleware):
exempt_paths = ('/health/', '/static/', '/favicon.ico')
def get_user_identity(self, request):
return {
'uid': request.user.sub,
'platform': request.user.platform,
'org': request.user.organization_uuid,
}
Development
# Clone and install
git clone https://github.com/Tunet-xyz/session_armor.git
cd session_armor
pip install -e ".[dev]"
# Run tests
pytest
# Lint
ruff check src/session_armor tests
# Type check
mypy src/session_armor
Public Agent MCP Contract
SessionArmor includes a public agent-operability contract in mcp/. It describes how external agents can safely help with middleware ordering, security settings, audit customization, gate design, browser-visible docs, and rollout planning without requiring source-code access or production session data.
From a checkout, run the dependency-free contract server with python mcp/server.py. After installation, agents can use the packaged stdio command:
sessionarmor-mcp
License
MIT — Copyright (c) 2024-2026 Tunet Ltd. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sessionarmor-0.5.0.tar.gz.
File metadata
- Download URL: sessionarmor-0.5.0.tar.gz
- Upload date:
- Size: 52.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd8e95be4ec35f0e47b3c50665d6786c4ea4e5aae4b2c0fa1de43d10e1f30db9
|
|
| MD5 |
1bb1e9c5f9b854da6902cde96def1e08
|
|
| BLAKE2b-256 |
83bef80255d7473c39989efc622bc67486697c95cb0b5dedb4da92b1ad807209
|
Provenance
The following attestation bundles were made for sessionarmor-0.5.0.tar.gz:
Publisher:
publish.yaml on Tunet-xyz/session_armor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sessionarmor-0.5.0.tar.gz -
Subject digest:
fd8e95be4ec35f0e47b3c50665d6786c4ea4e5aae4b2c0fa1de43d10e1f30db9 - Sigstore transparency entry: 2498412254
- Sigstore integration time:
-
Permalink:
Tunet-xyz/session_armor@88c6d08b2e035484563c76f5133b699e21a6a247 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Tunet-xyz
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@88c6d08b2e035484563c76f5133b699e21a6a247 -
Trigger Event:
release
-
Statement type:
File details
Details for the file sessionarmor-0.5.0-py3-none-any.whl.
File metadata
- Download URL: sessionarmor-0.5.0-py3-none-any.whl
- Upload date:
- Size: 31.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73f13bef8664586cdfcce6153e63483896c0799bc24a1be8136f764d7ef8bb02
|
|
| MD5 |
6b96c7a52b6653aca8974fdc09da519f
|
|
| BLAKE2b-256 |
c7e586dc12989cc4a306d67afc0587ebde93a53670f92d7e895fb63b71e7d23c
|
Provenance
The following attestation bundles were made for sessionarmor-0.5.0-py3-none-any.whl:
Publisher:
publish.yaml on Tunet-xyz/session_armor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sessionarmor-0.5.0-py3-none-any.whl -
Subject digest:
73f13bef8664586cdfcce6153e63483896c0799bc24a1be8136f764d7ef8bb02 - Sigstore transparency entry: 2498412260
- Sigstore integration time:
-
Permalink:
Tunet-xyz/session_armor@88c6d08b2e035484563c76f5133b699e21a6a247 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/Tunet-xyz
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@88c6d08b2e035484563c76f5133b699e21a6a247 -
Trigger Event:
release
-
Statement type: