File size: 6,680 Bytes
5ef3828
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# Security Policy

## Reporting a Vulnerability

If you find a security issue, please do **not** open a public GitHub issue.

Report privately via: [GitHub Security Advisories](https://github.com/VolkanSah/Universal-MCP-Hub/security/advisories/new)

Include: what you found, how to reproduce it, and what impact you think it has. You'll get a response within 72 hours. If the issue is confirmed, a fix will be prioritized before any public disclosure.

---

## Security Architecture

### The Guardian Pattern

`main.py` is the only process that ever touches secrets. It reads `.env` / HF Space Secrets, initializes `fundaments/*` conditionally, and passes the results into `app/*` as a validated dict. After that handoff, `app/*` has no path back to the raw environment.

```
.env / HF Secrets
      β”‚
      β–Ό
main.py (Guardian) β€” only entry point
      β”‚
      β”‚  initializes fundaments/* conditionally
      β”‚  injects as: fundaments = {"config": ..., "db": ..., "encryption": ..., ...}
      β”‚
      β–Ό
app/app.py β€” unpacks fundaments ONCE at startup, never stores globally
      β”‚
      └── app/* modules β€” read only from app/.pyfun, never from os.environ
```

This is enforced structurally. `app/*` has no import of `fundaments/`, no `os.environ` calls, no `.env` access. It's not a coding convention β€” it's an import boundary.

---

### Secret Handling

- API keys live exclusively in HF Space Secrets or `.env` β€” never in `app/.pyfun`, never in source code
- `app/.pyfun` references only ENV variable **names** (e.g. `env_key = "ANTHROPIC_API_KEY"`) β€” never values
- `list_active_tools` returns key names only β€” the actual key values are never exposed through any MCP tool
- `fundaments/` modules are initialized only if their required ENV vars are present β€” missing config = service skipped, not crashed

---

### Encryption (`fundaments/encryption.py`)

When `MASTER_ENCRYPTION_KEY` and `PERSISTENT_ENCRYPTION_SALT` are set:

- Key derivation: **PBKDF2-HMAC-SHA256**, 480,000 iterations, 256-bit output
- Encryption: **AES-256-GCM** β€” authenticated encryption, provides both confidentiality and integrity
- Each encryption operation uses a fresh random 96-bit nonce
- Authentication tag (128-bit) is stored alongside ciphertext β€” decryption fails loudly if data was tampered
- File encryption uses streaming with 8 KB chunks β€” nonce prepended, tag appended to file
- If encryption keys are not set, the service is skipped cleanly β€” the app runs without it

---

### Authentication (`fundaments/user_handler.py`)

- Passwords hashed with **PBKDF2-SHA256** via `passlib`
- Sessions stored in SQLite with IP address + User-Agent binding β€” `validate_session()` checks both
- Session IDs are regenerated on login (session fixation prevention)
- Failed login attempts tracked per user β€” account locks after 5 consecutive failures
- Session table uses `ON DELETE CASCADE` β€” deleting a user removes all their sessions automatically

---

### Access Control (`fundaments/access_control.py`)

- Role-based permission system backed by PostgreSQL (cloud DB, Guardian-only)
- Schema: `users β†’ user_role_assignments β†’ user_roles β†’ role_permissions β†’ user_permissions`
- All queries use parameterized statements via `asyncpg` β€” no string interpolation
- `has_permission()` checks the full role→permission chain per request
- `AccessControl` only initializes if a DB connection is available β€” degrades gracefully otherwise

---

### Database Security (`fundaments/postgresql.py`)

**Cloud DB (asyncpg pool):**
- SSL enforced at minimum `sslmode=require` β€” upgraded automatically if not set
- Neon.tech quirks handled: `statement_timeout` stripped from DSN (unsupported, causes silent failures), keepalives set
- Credentials masked in all log output β€” full DSN only in `DEBUG` level logs
- SSL runtime check via `pg_stat_ssl` on every new connection (cloud providers: warning only if unavailable)
- Automatic pool restart on Neon connection termination (sqlstate `08006`)
- Pool: min 1 / max 10 connections, connect timeout 5s, command timeout 30s

**Internal SQLite (`app/db_sync.py` + `fundaments/user_handler.py`):**
- Same SQLite file, separate tables β€” zero overlap enforced by code
- `users` + `sessions` β†’ owned by `user_handler.py` (Guardian layer)
- `hub_state` + `tool_cache` β†’ owned by `db_sync.py` (app/* layer)
- `db_query` MCP tool exposes SELECT-only access to `hub_state` and `tool_cache` β€” cannot reach `users` or `sessions`
- SELECT enforcement: non-SELECT queries raise `ValueError` before execution
- On HuggingFace Spaces: SQLite auto-relocated to `/tmp/` (HF filesystem is read-only outside `/tmp/`)

---

### MCP Transport Security

- All MCP traffic routes through Quart `/mcp` endpoint before reaching FastMCP
- This is the natural interception point for auth checks, rate limiting, and payload logging β€” none are optional add-ons
- `/crypto` endpoint exists but returns `501` until the app-layer encryption wrapper is implemented β€” not bypassed, not silently ignored
- `health_check` and `list_active_tools` are always available β€” they expose no secrets, only status and key names

---

### Sandboxing Rules

These rules are enforced by code structure, not just documentation:

| Rule | Enforced by |
| :--- | :--- |
| `app/*` never reads `os.environ` | No `os.environ` calls in any `app/` file |
| `app/*` never imports `fundaments/` | No cross-imports between layers |
| `fundaments/` services injected, not constructed in `app/*` | `start_application(fundaments)` signature |
| Direct execution of `app/app.py` blocked | `__main__` guard with null-fundaments warning |
| `db_query` tool is SELECT-only | `ValueError` raised before any non-SELECT execution |
| API keys never in `.pyfun` | `.pyfun` parser only reads key names, never resolves them |

---

### What This Project Does Not Claim

- It is not a penetration-tested security product
- The encrypted `/crypto` endpoint is not yet implemented
- Search provider implementations (Brave, Tavily) are pending β€” the tool registers as placeholder
- This is a personal/community project β€” use in production at your own assessment

> PyFundaments is not perfect. But it's more secure than most of what runs in production today.

---

## Supported Versions

| Version | Supported |
| :--- | :--- |
| latest (`main`) | βœ… |
| older forks | ❌ β€” fork at your own risk |

---

## License Note

This project is dual-licensed under Apache 2.0 and the [Ethical Security Operations License v1.1 (ESOL)](ESOL). Using this software for surveillance, unauthorized access, or harm to others automatically terminates your license.