Security Policy¶
Overview¶
Israeli Bank Importer handles sensitive financial credentials and data. This document outlines security best practices and our security policies.
🔐 Credential Security¶
Configuration File Protection¶
CRITICAL: Your config.json contains bank credentials and must be protected.
# Set restrictive permissions (owner read/write only)
chmod 600 config.json
# Verify permissions
ls -la config.json
# Should show: -rw------- (600)
Encrypted Config (Recommended)¶
For stronger protection, split and encrypt your config:
- Split config: separate
credentials.json(secrets) fromconfig.json(settings) -
Encrypt credentials:
CREDENTIALS_ENCRYPTION_PASSWORD=pass node scripts/encrypt-config.js credentials.json -
AES-256-GCM encryption (OWASP recommended)
- PBKDF2 key derivation (100,000 iterations)
- Decrypted only in memory — never written to disk
- Supports
CREDENTIALS_ENCRYPTION_PASSWORDenv var (and oldCONFIG_PASSWORD) - Even if someone steals the file, they can't read your credentials
Never Commit Credentials¶
- ✅
config.jsonis in.gitignoreby default - ❌ NEVER commit
config.jsonto version control - ❌ NEVER share
config.jsonpublicly - ❌ NEVER paste config contents in issues or forums
Environment Variables (Alternative)¶
For CI/CD or automated deployments, consider using environment variables instead of config.json:
# Example for Bank Discount
ACTUAL_SERVER_URL=https://your-server.com
ACTUAL_PASSWORD=your_password
DISCOUNT_ID=your_id
DISCOUNT_PASSWORD=your_password
DISCOUNT_NUM=your_num
🐳 Docker Security¶
Required Capabilities¶
The importer requires SYS_ADMIN capability for Camoufox (Firefox) browser sandboxing:
Why is this needed?
- Camoufox (Firefox) requires kernel namespaces for browser sandboxing
- This is standard for headless browser automation
- Without it, the browser cannot launch
Is this safe?
- ✅ Safe when running trusted code (this open-source project)
- ✅ Container still runs as non-root user (
node) - ✅ No other privileged operations are performed
- ⚠️ Only run trusted Docker images with this capability
Volume Mounts¶
Mount config.json as read-only:
This prevents the container from modifying your configuration file.
Container User¶
The Dockerfile runs as non-root user node (UID 1000):
This follows the principle of least privilege.
Directory Permissions¶
As of v1.0, directories use secure permissions:
# Before: drwxrwxrwx (777) - INSECURE
# After: drwxr-xr-x (755) - SECURE
# Owner (node): read, write, execute
# Group: read, execute
# Others: read, execute
🔒 Data Storage¶
Persistent Volumes¶
The importer uses these volumes:
| Volume | Purpose | Sensitivity |
|---|---|---|
config.json | Bank credentials | 🔴 HIGH |
data/ | Actual Budget cache, long-term bank tokens, import history, OTP requests, push device tokens | 🔴 HIGH |
cache/ | Scraper cache | 🟡 MEDIUM |
chrome-data/ | Browser profile (legacy, unused with Camoufox) | 🟢 LOW |
Recommendations:
- Store volumes on encrypted filesystem
- Limit access to authorized users only
- Regular backups of
config.json(stored securely) - Delete old cache periodically
Data at Rest¶
- Config file contains plain-text credentials
- The portal saves
config.jsonandcredentials.jsonowner-only (0600). Each is written under a random name, created exclusively, then renamed into place, so a link planted beside it is never followed. A__proto__key in either file is ignored when it is read - The app sign-ins (
app-tokens.json, on the data volume) are owner-only (0600) and hold a SHA-256 hash of each app refresh token, never the token itself. A file an earlier release left readable to others is made owner-only the next time it is read. The portal never writes over a token file it cannot read: an app sign-in or refresh then fails with a server error instead of signing every phone out - A portal server error never carries its cause: it answers a fixed message such as
Internal server errororFailed to persist configuration, and the cause (a file path, an error code) goes to the portal log bank-tokens.jsonon the data volume is an owner-only (0600) file of long-term tokens that skip the SMS login. WithCREDENTIALS_ENCRYPTION_PASSWORD(or the legacyCONFIG_PASSWORD) set, each token the importer saves is sealed under it; without either, each token it saves is plain text. Delete a plain-textbank-tokens.jsonwhen you turn the password onbank-tokens.refused.json, beside it, is owner-only (0600) too. It holds a one-way SHA-256 digest of each long-term token the importer retired, never the token, so it is not sealed- Four more files on the data volume are owner-only (
0600) too: the import history (audit-log.json), OTP requests and their answers (otp-requests.<id>.jsonandotp-requests.<id>.answer.json), the OTP channel (otp-settings.json) and the push device tokens (devices.json). A file an earlier release left readable to others is made owner-only the next time it is read. The import history, the OTP channel and the device tokens are replaced in one atomic step on every write. An OTP request or answer appears whole under a name that can be taken only once and is not rewritten, except that a used code's answer is replaced, in one atomic step, by a marker. A damaged file is moved aside (<name>.quarantined-…) before the next write; an OTP file is not written again, so a damaged one is left in place. When the importer or the portal starts, it removes staged leftovers older than an hour from the files it writes, OTP requests and answers an hour past their deadline, and a damaged OTP file an hour after its last write - Consider using encrypted filesystem (LUKS, BitLocker, etc.)
- On VMs, use encrypted EBS/disk volumes
Data in Transit¶
- ✅ Bank scrapers use HTTPS
- ✅ Actual Budget API uses HTTPS (if configured)
- ⚠️ Ensure your Actual server uses HTTPS in production
🚨 Vulnerability Reporting¶
Reporting Security Issues¶
DO NOT open public GitHub issues for security vulnerabilities.
Instead, use GitHub Security Advisories:
- Go to the Security tab on the repository
- Click "Report a vulnerability"
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work on a fix.
Security Updates¶
Security patches will be released as:
- Critical: Immediate patch release (e.g., v1.0.1)
- High: Within 1 week
- Medium/Low: Next minor release
Incident Response¶
If a security vulnerability is discovered in a released version:
- Triage (within 24 hours): Assess severity and impact
- Containment: If critical, publish a GitHub Security Advisory immediately
- Fix: Develop and test a patch on a private branch
- Release: Ship as a patch release with
fix:conventional commit - Notify: Update the GitHub Security Advisory with fix details
🛡️ Best Practices¶
For End Users¶
- Keep Docker Image Updated
- Use Strong Passwords
- Use unique passwords for Actual Budget server
-
Enable 2FA on bank accounts where available
-
Monitor Logs
- Check for failed login attempts
-
Check for unusual activity
-
Limit Network Access
- Run on private network if possible
- Use firewall rules to restrict outbound connections
-
Only allow connections to: banks, Actual server
-
Regular Security Audits
- Review
config.jsonperiodically - Remove unused bank configurations
- Check file permissions
For Developers¶
- Dependency Scanning
- Code Review
- All PRs require review
-
Security-sensitive changes require maintainer approval
-
No Hardcoded Secrets
- Never commit credentials
-
Use environment variables or volume mounts
-
Principle of Least Privilege
- Request minimum Docker capabilities
- Run as non-root user
- Use minimal base image
🔍 Security Features¶
Current Security Measures¶
Container Security¶
- ✅ Non-root container user (
node, UID 1000) - ✅ Secure directory permissions (755)
- ✅ Read-only application files (cannot be tampered)
- ✅ Base image pinned to digest (supply chain protection)
- ✅ Minimal base image (node:26-slim)
- ✅ Health checks enabled (auto-restart on failure)
- ✅
cap_drop: ALL+ onlySYS_ADMIN(least privilege) - ✅
no-new-privilegessecurity option - ✅ Dedicated
/dev/shmtmpfs for browser shared memory
Application Security¶
- ✅ Config file not baked into image
- ✅ Read-only config mount supported (
:ro) - ✅ Credentials never logged
- ✅
.gitignoreprevents credential commits - ✅ Comprehensive input validation (UUIDs, URLs, dates, emails)
Dependency Security¶
- ✅
npm run audit: blocking CI check (fails at MODERATE+). An advisory passes only if it is fixed, or carries an entry inconfig/audit-policy.mjsthat states a non-emptyreasonand whoseexpiresis a real calendar date still in the future. An entry covering a package that reaches the production tree is an accepted risk rather than a deferral, so it must additionally setproductionReachableandnoUpstreamFix, carry anaddeddate no later than today, give an HTTPSupstreamlink to the work being tracked, and expire within 30 days ofadded. A development-tree entry may instead setautoExtend: { fixedIn }: from itsexpiresdate on, it stands only while a package in the committed lockfile declares a range that cannot reachfixedIn, and every audit run warns about it (see ADR-0004). An auto-extending entry is never honoured in the production tree. - ✅ Automated dependency updates via Dependabot (npm, Actions, Docker). Safe patch and minor updates merge on their own after every required check passes; majors, Docker minors and script-pin rotations wait for a human (see ADR-0002)
- ✅ CodeQL static analysis on every PR
- ✅ Latest security patches applied (Node.js 26, TypeScript 6)
Build Security¶
- ✅ Digest-pinned base image (immutable, reproducible)
- ✅ npm, npx, corepack and yarn removed from the runtime image, so their bundled dependency trees cannot carry unpatchable CVEs
- ✅ Multi-stage security in CI/CD
- ✅ Automated vulnerability scanning
- ✅ OWASP Docker Security compliance: 9/10
Supply Chain Security¶
- ✅ SBOM (Software Bill of Materials) attached to every GitHub Release (SPDX format)
- ✅ Docker images scanned with Trivy on every PR (CRITICAL + HIGH, blocks merge)
- ✅ Automated dependency updates via Dependabot (npm, GitHub Actions, Docker), with safe patch and minor updates auto-merged after CI
- ✅ Weekly lockfile refresh updates transitive dependencies and applies
npm audit fixchanges within declared ranges, then auto-merges after CI unless a pin crossed a major - ✅
mainis audited every four hours. An advisory behind an exact transitive pin gets anoverridesfloor, which is proved to clear it before the pull request opens. Aged same-line^floors auto-merge; the rest, including any raised>=override, wait for a human (see ADR-0003) - ✅
npm run auditruns as blocking CI check (no bypass; exceptions expire, and an opted-in development-tree exception outlives its date only while the committed lockfile blocks the fix)
📋 Security Checklist¶
Before deploying to production:
- Set
chmod 600 config.json - Mount config as read-only (
:ro) - Use HTTPS for Actual Budget server
- Enable 2FA on bank accounts
- Run on private/secure network
- Use encrypted filesystem/volumes
- Regular Docker image updates
- Monitor logs for anomalies
- Backup
config.jsonsecurely - Limit
SYS_ADMINto trusted images only
📚 Additional Resources¶
📄 License¶
This security policy is part of the Israeli Bank Importer project and is licensed under the MIT License.
🔄 Security Update History¶
v1.4.1 (2026-02-18) - Security Hardening¶
- ✅ Base image pinned to digest for supply chain protection
- ✅ OS security patches applied (
apt-get upgrade) - ✅ npm updated from 10.x to 11.x (major security update)
- ✅ TypeScript updated to 5.9.3
- ✅ File permissions hardened (read-only application files)
- ✅ Docker health checks added
- ✅ Automated security scanning in CI/CD
- ✅ npm audit: 0 vulnerabilities
v1.4.0 (2026-02-18) - Observability¶
- ✅ Configuration validation at startup
- ✅ Metrics collection for monitoring
v1.3.0 (2026-02-18) - TypeScript Migration¶
- ✅ Full type safety
- ✅ Error handling improvements
Last Updated: 2026-02-21