Information Security Policy
Last reviewed 2026-08-14 · Owner: Dylan Coats (Owner / Information Security Officer) · Coats Labs
Scope
This policy covers Stewardly, a household budgeting application that
stores users' financial account data (balances, transactions) and
connects to a third-party financial data aggregator (SimpleFIN) to
retrieve it. It applies to the production application, its database,
and the infrastructure it runs on.
Risk identification, mitigation, and monitoring
As a single-developer project, risk identification happens primarily
through deliberate code review during development rather than a
separate scheduled audit process — every change that touches
authentication, financial data access, or secrets handling is reviewed
against this policy before it ships. Runtime risk is monitored via the
access log described below and via Azure's own platform-level
monitoring for the database, Key Vault, and Function App. This is an
area intended to mature as the project grows (see "Planned
improvements" at the end of this page).
Data classification
- Financial data (account balances, transactions, institution connections) — the highest-sensitivity data the app holds. Scoped by household at the database query level; every query that touches this data filters by household, so one household can never read another's.
- Credentials and secrets (passwords, bank-connection access tokens/URLs, TOTP secrets) — never stored in plaintext. See Encryption below.
- Account/contact data (usernames, names, phone numbers) — moderate sensitivity, household-scoped the same way.
Access control
- Role-based access control. Each user is either a regular household member or a household admin. Actions that affect the whole household — connecting/reconnecting a bank, managing other users, billing — are gated to admins only at the API layer, not just hidden in the UI.
- Tenant isolation. Every data-access query is scoped to the caller's own household, enforced server-side on every request, not by client-side filtering.
- Session-based authorization. API requests carry a signed session token validated on every call; a session is immediately invalidated household-member-wide on password reset via a server-side session-version check, so a compromised or stale token stops working the moment a password is changed.
- Non-human authentication. The application's own backend authenticates to its secrets store (Azure Key Vault) using Azure Managed Identity, which is OAuth 2.0-token-based — no static credential is embedded in the application for that purpose. All network traffic, human and service-to-service alike, is TLS-encrypted end to end via Azure's platform.
Authentication
- Passwords: minimum 10 characters, must include upper case, lower case, a digit, and a symbol; hashed with bcrypt, never stored or logged in plaintext.
- Account lockout: 5 consecutive failed login attempts locks that username out for 15 minutes, independent of whether the eventual password would have been correct.
- IP-based rate limiting: unauthenticated auth endpoints (login, 2FA challenge, passkey login) are additionally rate-limited per source IP; authenticated traffic is covered by a separate, more generous global per-IP limit as a flood backstop.
- Multi-factor authentication: the app supports two independent MFA methods, available to every user on a self-service, opt-in basis from their account settings — TOTP (standard 6-digit authenticator-app codes, with one-time-use bcrypt-hashed recovery codes issued at setup), and WebAuthn/FIDO2 passkeys (Face ID, Touch ID, Windows Hello, or a hardware security key), which are phishing-resistant by construction since the credential is cryptographically bound to the app's own origin and cannot be replayed against a look-alike site.
MFA is not mandatory for regular account use, but it is mandatory before the specific action of connecting a new bank account: an admin cannot establish a new bank-data connection (via SimpleFIN) unless TOTP or a passkey is already configured on their own account. Enforced server-side, not just hidden in the UI.
Encryption
- At rest: bank-connection access credentials and TOTP secrets are encrypted with AES-256-GCM before being written to the database, using a key held in Azure Key Vault and never checked into source control. Each encrypted value carries its own random IV and authentication tag.
- In transit: all traffic to and from the application, and from the application to the database and to Key Vault, is TLS-encrypted via the Azure platform (Static Web Apps, Functions, Postgres Flexible Server, and Key Vault all enforce TLS by default).
Secrets management
All secrets (database credentials, encryption keys, third-party API
credentials) live in Azure Key Vault, never in source control or
plaintext configuration files. The Function App authenticates to Key
Vault via its Azure Managed Identity — no long-lived Key Vault
credential exists anywhere in the application itself.
Secure development practices
- All database queries use parameterized statements exclusively; no string-concatenated SQL exists anywhere in the codebase, which rules out SQL injection as an attack surface.
- Every deployed change is syntax-checked before deployment; the application is deployed from a version-controlled repository, and every change to authentication, access control, or data handling is manually reviewed against this policy before shipping.
Vulnerability management
- Dependency scanning: the repository runs automated weekly scans (GitHub Dependabot) against every third-party package the application depends on, opening a pull request automatically when a known vulnerability (CVE) is found in a package version in use.
- Patch SLA: critical- and high-severity dependency vulnerabilities are patched within 7 days of the alert; medium- and low-severity within 30 days.
- Platform-managed patching: the underlying operating system and managed runtime for both the database (Azure Database for PostgreSQL Flexible Server) and the compute layer (Azure Functions) are patched by Azure itself as part of the managed-service agreement — there is no customer-managed server or OS for either to fall behind on.
- Injection-class vulnerabilities are eliminated by construction: every database query in the codebase uses parameterized statements; there is no string-concatenated SQL anywhere, which removes SQL injection as a possible vulnerability class rather than relying on scanning to catch it.
- Endpoint (developer-machine) scanning does not apply — there is no device fleet; the sole developer's own machine is kept current via standard OS auto-update.
Monitoring and logging
Every authentication event (successful login, failed login, lockout,
2FA/passkey failure) is recorded in an access log with timestamp,
username, outcome, source IP, and user agent, scoped per household and
visible to that household's admins. This is the primary detective
control for credential abuse today.
Incident response
In the event of a suspected compromise:
- The affected user's password is reset, which immediately invalidates their existing session(s) household-member-wide via the session-version mechanism.
- If a shared secret (encryption key, API credential) is suspected compromised, it is rotated in Key Vault immediately.
- Affected users are notified directly by the application owner.
- The access log is reviewed to scope the extent of any unauthorized access.
Planned improvements
- Formalize a periodic (e.g. quarterly) self-review of access grants and dependency versions, rather than the current review-on-change model.
← Back to Stewardly
·
Privacy Policy