wimotek-edge-agent 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/wimotek_edge_agent/agent.py +24 -1
- package/wimotek_edge_agent/audit.py +60 -3
- package/wimotek_edge_agent/config.py +10 -0
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ Commercial-grade edge access control, service bridge, and tunnel agent featuring
|
|
|
14
14
|
- [Programmatic usage](#programmatic-usage)
|
|
15
15
|
- [Reverse tunnel / NAT4](#reverse-tunnel--nat4)
|
|
16
16
|
- [Audit log](#audit-log)
|
|
17
|
+
- [Storage Mechanics & Format](#1-storage-mechanics--format)
|
|
18
|
+
- [Risks of Unmanaged Log Growth](#2-risks-of-unmanaged-log-growth)
|
|
19
|
+
- [Log Rotation & Cleanup Solutions](#3-log-rotation--cleanup-solutions)
|
|
17
20
|
- [Security model](#security-model)
|
|
18
21
|
- [Commercial deployment checklist](#commercial-deployment-checklist)
|
|
19
22
|
- [License](#license)
|
|
@@ -241,6 +244,8 @@ npx wimotek-edge-agent --help
|
|
|
241
244
|
| `EDGE_AGENT_SESSION_TICKET_TTL_SECONDS` | no | `300` | Session ticket TTL (seconds) |
|
|
242
245
|
| `EDGE_AGENT_TOFU_HEARTBEAT_SECONDS` | no | `10` | TOFU quarantine heartbeat interval |
|
|
243
246
|
| `EDGE_AGENT_REQUIRE_LOCAL_BYPASS_TOKEN` | no | `true` | Require local bypass token for loopback |
|
|
247
|
+
| `EDGE_AGENT_AUDIT_MAX_BYTES` | no | `10485760` (10MB) | Max audit log size before auto-rotation |
|
|
248
|
+
| `EDGE_AGENT_AUDIT_BACKUP_COUNT` | no | `5` | Number of rotated audit log files to keep |
|
|
244
249
|
| `EDGE_AGENT_REVERSE_TUNNEL` | no | auto | Force reverse tunnel (`true`/`false`) |
|
|
245
250
|
| `EDGE_AGENT_NAT_TYPE` | no | `unknown` | Network NAT type (`nat4`, `symmetric`, etc.) |
|
|
246
251
|
| `EDGE_AGENT_NODE_UUID` | reverse only | - | Agent node UUID |
|
|
@@ -310,6 +315,16 @@ Read or generate the local bypass token:
|
|
|
310
315
|
wimotek-edge-agent bypass-token
|
|
311
316
|
```
|
|
312
317
|
|
|
318
|
+
Clean or truncate the audit log (keep recent N lines or 0 for complete truncate):
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Keep latest 500 lines:
|
|
322
|
+
wimotek-edge-agent clean-audit --keep 500
|
|
323
|
+
|
|
324
|
+
# Completely wipe audit log:
|
|
325
|
+
wimotek-edge-agent clean-audit --keep 0
|
|
326
|
+
```
|
|
327
|
+
|
|
313
328
|
## Programmatic usage
|
|
314
329
|
|
|
315
330
|
```python
|
|
@@ -364,13 +379,76 @@ await agent.run_reverse_tunnel(
|
|
|
364
379
|
|
|
365
380
|
## Audit log
|
|
366
381
|
|
|
367
|
-
|
|
382
|
+
### 1. Storage Mechanics & Format
|
|
383
|
+
|
|
384
|
+
Every `AccessDecision` and administrative action is written to `$EDGE_AGENT_DATA_DIR/audit.jsonl` (default: `./edge_agent_data/audit.jsonl`).
|
|
385
|
+
Lines are formatted as standard **JSON Lines (JSONL)** and protected by a thread-safe mutex lock:
|
|
368
386
|
|
|
369
387
|
```json
|
|
370
388
|
{"ts":"2026-11-06T12:00:00+00:00","event":"access_decision","node_uuid":"node-xxx","source_ip":"203.0.113.10","is_loopback":false,"decision":"allow_token","reason":"edge_agent_token_verified","network_profile":"direct","nat_type":"unknown","fingerprint":"sha256:...","node_status":"trusted"}
|
|
371
389
|
```
|
|
372
390
|
|
|
373
|
-
|
|
391
|
+
Raw join tokens are never logged.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
### 2. Risks of Unmanaged Log Growth
|
|
396
|
+
|
|
397
|
+
In high-throughput edge environments (hundreds of handshakes/sec):
|
|
398
|
+
1. **Disk Exhaustion (ENOSPC)**: Without rotation, `audit.jsonl` can grow to tens of gigabytes, exhausting device storage.
|
|
399
|
+
2. **Database Failure**: When disk fills up, SQLite (`trust_store.sqlite3`) fails during WAL commits, causing trust state and replay tracking to crash.
|
|
400
|
+
3. **Memory & I/O Spikes**: Reading massive un-rotated log files causes severe memory consumption and potential OOM kills.
|
|
401
|
+
4. **Compliance & SIEM Delay**: Retaining only raw local logs without shipping violates enterprise zero-trust audit policies.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
### 3. Log Rotation & Cleanup Solutions
|
|
406
|
+
|
|
407
|
+
#### A. Built-in Automatic Size Rotation (Recommended)
|
|
408
|
+
`AuditLogger` features built-in auto-rotation when file size reaches the threshold:
|
|
409
|
+
- Automatically rolls into `audit.jsonl.1` through `audit.jsonl.5`, purging the oldest file.
|
|
410
|
+
- Configurable via environment variables:
|
|
411
|
+
```bash
|
|
412
|
+
# Maximum file size before rotation (default: 10485760 = 10MB)
|
|
413
|
+
export EDGE_AGENT_AUDIT_MAX_BYTES=10485760
|
|
414
|
+
# Number of rotated archives to retain (default: 5)
|
|
415
|
+
export EDGE_AGENT_AUDIT_BACKUP_COUNT=5
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
#### B. CLI Manual Truncate & Clear
|
|
419
|
+
```bash
|
|
420
|
+
# Keep latest 500 lines and safely truncate history:
|
|
421
|
+
wimotek-edge-agent clean-audit --keep 500
|
|
422
|
+
|
|
423
|
+
# Completely wipe audit log:
|
|
424
|
+
wimotek-edge-agent clean-audit --keep 0
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### C. Web Console 1-Click Clean
|
|
428
|
+
In the Edge Agent Web Console:
|
|
429
|
+
- Navigate to **"安全审计 (Audit Log)"** tab.
|
|
430
|
+
- Click the red **"清理日志 (Clear Logs)"** button to truncate safely with confirmation.
|
|
431
|
+
|
|
432
|
+
#### D. Production Linux Logrotate Configuration
|
|
433
|
+
For systemd/daemon deployments, configure `/etc/logrotate.d/wimotek-edge-agent`:
|
|
434
|
+
|
|
435
|
+
```text
|
|
436
|
+
/path/to/edge_agent_data/audit.jsonl {
|
|
437
|
+
daily
|
|
438
|
+
missingok
|
|
439
|
+
rotate 7
|
|
440
|
+
compress
|
|
441
|
+
delaycompress
|
|
442
|
+
notifempty
|
|
443
|
+
copytruncate
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
*(The `copytruncate` directive allows safe truncation without interrupting running agent processes).*
|
|
447
|
+
|
|
448
|
+
#### E. SIEM Central Log Shipping
|
|
449
|
+
Stream JSONL lines via Vector, FluentBit, or Filebeat directly to Loki, Elasticsearch, or AWS CloudWatch.
|
|
450
|
+
|
|
451
|
+
---
|
|
374
452
|
|
|
375
453
|
## Security model
|
|
376
454
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -40,7 +40,11 @@ class EdgeAgent:
|
|
|
40
40
|
self.config.ensure_dirs()
|
|
41
41
|
self.data_handler = data_handler
|
|
42
42
|
|
|
43
|
-
self.audit = AuditLogger(
|
|
43
|
+
self.audit = AuditLogger(
|
|
44
|
+
config.audit_log_path,
|
|
45
|
+
max_bytes=config.audit_max_bytes,
|
|
46
|
+
backup_count=config.audit_backup_count,
|
|
47
|
+
)
|
|
44
48
|
self.trust_store = TrustStore(config.trust_db_path)
|
|
45
49
|
self.local_bypass = LocalBypassManager(
|
|
46
50
|
config.local_bypass_token_path,
|
|
@@ -451,6 +455,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
451
455
|
p_sim.add_argument("--reverse-tunnel", action="store_true")
|
|
452
456
|
p_sim.add_argument("--nat-type", default="unknown")
|
|
453
457
|
|
|
458
|
+
# 13. clean-audit
|
|
459
|
+
p_clean = subparsers.add_parser("clean-audit", help="Truncate or clear audit log")
|
|
460
|
+
p_clean.add_argument(
|
|
461
|
+
"--keep",
|
|
462
|
+
type=int,
|
|
463
|
+
default=0,
|
|
464
|
+
help="Keep last N records (default 0, clears all records)",
|
|
465
|
+
)
|
|
466
|
+
|
|
454
467
|
return parser
|
|
455
468
|
|
|
456
469
|
|
|
@@ -491,6 +504,16 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
491
504
|
}, indent=2))
|
|
492
505
|
return 0
|
|
493
506
|
|
|
507
|
+
if args.command == "clean-audit":
|
|
508
|
+
retained = agent.audit.clear(keep_last_lines=args.keep)
|
|
509
|
+
agent.audit.emit("audit_log_cleaned", retained_lines=retained, keep_requested=args.keep)
|
|
510
|
+
print(json.dumps({
|
|
511
|
+
"status": "ok",
|
|
512
|
+
"message": f"Audit log cleaned (retained {retained} lines)",
|
|
513
|
+
"retained_lines": retained,
|
|
514
|
+
}, indent=2))
|
|
515
|
+
return 0
|
|
516
|
+
|
|
494
517
|
if args.command == "stats":
|
|
495
518
|
stats = agent.trust_store.get_stats()
|
|
496
519
|
print(json.dumps(stats, indent=2))
|
|
@@ -9,14 +9,45 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
class AuditLogger:
|
|
11
11
|
"""
|
|
12
|
-
|
|
12
|
+
结构化审计日志管理器。
|
|
13
|
+
支持自动按大小轮转 (Auto-Rotation) 与安全截断清理,防止未清理导致磁盘耗尽。
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
|
-
def __init__(
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
path: Path,
|
|
19
|
+
max_bytes: int = 10 * 1024 * 1024,
|
|
20
|
+
backup_count: int = 5,
|
|
21
|
+
) -> None:
|
|
16
22
|
self.path = path
|
|
17
23
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
self.max_bytes = max_bytes
|
|
25
|
+
self.backup_count = backup_count
|
|
18
26
|
self._lock = threading.Lock()
|
|
19
27
|
|
|
28
|
+
def _rotate_if_needed(self) -> None:
|
|
29
|
+
if self.max_bytes <= 0 or self.backup_count <= 0:
|
|
30
|
+
return
|
|
31
|
+
try:
|
|
32
|
+
if not self.path.exists() or self.path.stat().st_size < self.max_bytes:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
for i in range(self.backup_count - 1, 0, -1):
|
|
36
|
+
sfn = self.path.with_name(f"{self.path.name}.{i}")
|
|
37
|
+
dfn = self.path.with_name(f"{self.path.name}.{i + 1}")
|
|
38
|
+
if sfn.exists():
|
|
39
|
+
if dfn.exists():
|
|
40
|
+
dfn.unlink()
|
|
41
|
+
sfn.rename(dfn)
|
|
42
|
+
|
|
43
|
+
dfn = self.path.with_name(f"{self.path.name}.1")
|
|
44
|
+
if dfn.exists():
|
|
45
|
+
dfn.unlink()
|
|
46
|
+
self.path.rename(dfn)
|
|
47
|
+
except Exception:
|
|
48
|
+
# 保证主线程写日志不因归档异常而崩溃
|
|
49
|
+
pass
|
|
50
|
+
|
|
20
51
|
def emit(self, event: str, **fields: Any) -> None:
|
|
21
52
|
record = {
|
|
22
53
|
"ts": datetime.now(timezone.utc).isoformat(),
|
|
@@ -25,5 +56,31 @@ class AuditLogger:
|
|
|
25
56
|
}
|
|
26
57
|
line = json.dumps(record, ensure_ascii=False, default=str, sort_keys=True)
|
|
27
58
|
with self._lock:
|
|
59
|
+
self._rotate_if_needed()
|
|
28
60
|
with self.path.open("a", encoding="utf-8") as f:
|
|
29
|
-
f.write(line + "\n")
|
|
61
|
+
f.write(line + "\n")
|
|
62
|
+
|
|
63
|
+
def clear(self, keep_last_lines: int = 0) -> int:
|
|
64
|
+
"""
|
|
65
|
+
清理或截断审计日志。
|
|
66
|
+
- keep_last_lines = 0: 完全清空
|
|
67
|
+
- keep_last_lines > 0: 保留最近 N 行
|
|
68
|
+
返回清理后保留的行数。
|
|
69
|
+
"""
|
|
70
|
+
with self._lock:
|
|
71
|
+
if not self.path.exists():
|
|
72
|
+
return 0
|
|
73
|
+
if keep_last_lines <= 0:
|
|
74
|
+
self.path.write_text("", encoding="utf-8")
|
|
75
|
+
return 0
|
|
76
|
+
try:
|
|
77
|
+
content = self.path.read_text(encoding="utf-8")
|
|
78
|
+
lines = [l for l in content.splitlines() if l.strip()]
|
|
79
|
+
remaining = lines[-keep_last_lines:]
|
|
80
|
+
self.path.write_text(
|
|
81
|
+
"\n".join(remaining) + ("\n" if remaining else ""),
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
return len(remaining)
|
|
85
|
+
except Exception:
|
|
86
|
+
return 0
|
|
@@ -44,6 +44,10 @@ class Config:
|
|
|
44
44
|
session_ticket_ttl_seconds: int = 300
|
|
45
45
|
tofu_heartbeat_seconds: int = 10
|
|
46
46
|
|
|
47
|
+
# 审计日志自动轮转配置
|
|
48
|
+
audit_max_bytes: int = 10 * 1024 * 1024 # 默认 10MB
|
|
49
|
+
audit_backup_count: int = 5 # 默认保留 5 个归档
|
|
50
|
+
|
|
47
51
|
# None 表示自动判断;True 表示强制反向隧道;False 表示强制直连。
|
|
48
52
|
reverse_tunnel: Optional[bool] = None
|
|
49
53
|
nat_type: Optional[str] = None
|
|
@@ -99,6 +103,12 @@ class Config:
|
|
|
99
103
|
tofu_heartbeat_seconds=int(
|
|
100
104
|
os.getenv("EDGE_AGENT_TOFU_HEARTBEAT_SECONDS", "10")
|
|
101
105
|
),
|
|
106
|
+
audit_max_bytes=int(
|
|
107
|
+
os.getenv("EDGE_AGENT_AUDIT_MAX_BYTES", str(10 * 1024 * 1024))
|
|
108
|
+
),
|
|
109
|
+
audit_backup_count=int(
|
|
110
|
+
os.getenv("EDGE_AGENT_AUDIT_BACKUP_COUNT", "5")
|
|
111
|
+
),
|
|
102
112
|
reverse_tunnel=_env_opt_bool("EDGE_AGENT_REVERSE_TUNNEL"),
|
|
103
113
|
nat_type=os.getenv("EDGE_AGENT_NAT_TYPE") or None,
|
|
104
114
|
tls_server_name=os.getenv("EDGE_AGENT_TLS_SERVER_NAME") or None,
|