Skip to content

Commit 328cca4

Browse files
authored
Merge pull request #5 from ExpTechTW/main
Fix SSH Config
2 parents a8795fd + bef9a73 commit 328cca4

33 files changed

Lines changed: 3402 additions & 1021 deletions

COMMAND_ZH.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ sudo kekkai show /tmp/test.yaml # 指定檔案
104104
```
105105

106106
用途:
107-
- 檢查 `security.enforce_ssh_private` 是否把 22 自動加進 `private.tcp`
107+
- 檢查 `security.allow_ssh_public` 22 自動加進 `public.tcp` 還是 `private.tcp`
108108
- v1 遷移時預覽新版長怎樣(但要 daemon 正式啟動才會真的寫回)
109109
- Diff 兩個 config 找差異
110110

@@ -299,7 +299,7 @@ journalctl -u kekkai-agent -p err # 只看 error 級別
299299
Reload 成功時 journal 會記:
300300
- `filter applied: public tcp=[...] udp=[...] ...`
301301
- `auto-backup written: /etc/kekkai/kekkai.yaml.auto_backup.20260414T...`(如果 struct 有變化)
302-
- `normalize: auto-added port 22 to filter.private.tcp (security.enforce_ssh_private=true)`(如果 Normalize 動到 config)
302+
- `normalize: auto-added port 22 to filter.public.tcp (security.allow_ssh_public=true)`(如果 Normalize 動到 config`allow_ssh_public=false` 時則加進 `filter.private.tcp`
303303

304304
### 4.3 熱重載 vs 重啟
305305

@@ -372,17 +372,16 @@ observability:
372372
stats_file: /var/run/kekkai/stats.txt
373373

374374
security:
375-
enforce_ssh_private: true # 自動把 22 放進 private.tcp
376-
allow_ssh_public: false # 允許 22 放 public.tcp(危險)
375+
allow_ssh_public: true # true=自動把 22 放 public.tcp(預設);false=放 private.tcp
377376

378377
filter:
379378
public:
380-
tcp:
379+
tcp: # allow_ssh_public=true 時 22 會被 normalize 自動加
381380
- 80
382381
- 443
383382
udp:
384383
private:
385-
tcp: # 22 會被 normalize 自動加
384+
tcp: # allow_ssh_public=false 時 22 改加在這
386385
udp:
387386
ingress_allowlist: # private 服務的來源白名單
388387
- 10.0.0.0/8
@@ -420,14 +419,14 @@ Load / reload 都會跑:
420419
- 所有 port 在 1..65535
421420
- 同一 proto 的 port 不能在 public 和 private 同時出現
422421
- 所有 CIDR 必須解析成功
423-
- SSH 安全檢查
424-
- `allow_ssh_public=false` 22 在 `public.tcp`**拒絕啟動**
425-
- 22 同時在 public 和 private**拒絕啟動**
426-
- 22 `private.tcp` `ingress_allowlist`**拒絕啟動**SSH lockout 防護
422+
- SSH (port 22) 歸位必須和 `allow_ssh_public` 一致(不填則自動歸位)
423+
- `allow_ssh_public: true` 22 在 `private.tcp`**拒絕啟動**
424+
- `allow_ssh_public: false` 但 22 在 `public.tcp`**拒絕啟動**
425+
- 22 同時在 `public.tcp` `private.tcp` **拒絕啟動**一般「同 port 不能跨 public/private」規則
427426

428427
### 5.6 Normalize 行為
429428

430-
`security.enforce_ssh_private: true`(預設)時,若 22 既不在 `public.tcp` 也不在 `private.tcp`會自動加進 `private.tcp`。log 會記 `normalize: auto-added port 22 to filter.private.tcp`
429+
`security.allow_ssh_public` 決定 22 自動歸到哪個 group:若 22 既不在 `public.tcp` 也不在 `private.tcp``true`(預設)時自動加進 `public.tcp``false` 時加進 `private.tcp`你若已手動列入某個 group 則不動。log 會記 `normalize: auto-added port 22 to filter.public.tcp`(或 `.private.tcp`
431430

432431
---
433432

@@ -537,10 +536,11 @@ sudo kekkai status # 看 TUI
537536
1. 從 `https://github.com/ExpTechTW/kekkai/releases` 抓目標 channel(release / pre-release)的最新資產
538537
2. 下載 `kekkai-agent-linux-<arch>` 和 `kekkai-linux-<arch>` 到 tmp 目錄
539538
3. 用新 agent binary 跑 `-check` 驗 `/etc/kekkai/kekkai.yaml`(失敗中止,不動 service)
540-
4. 三路 diff:agent / cli / kekkai.sh 個別比對 sha256,每個獨立決定要不要更新
541-
5. agent 有變 → `systemctl restart kekkai-agent`(失敗自動 rollback 到 `kekkai-agent.prev`)
542-
6. cli 或 kekkai.sh 有變 → 個別覆寫,不 restart service
543-
7. 最後印藍色 `UPDATED`(有變)或綠色 `ALREADY UP-TO-DATE`(全部沒變)結果區塊
539+
4. 只有在 `config version` 變更時,才用新 agent binary 產生 canonical config;先備份再寫回 `/etc/kekkai/kekkai.yaml`(保留原有值並補新欄位)
540+
5. 三路 diff:agent / cli / kekkai.sh 個別比對 sha256,每個獨立決定要不要更新
541+
6. agent 有變 → `systemctl restart kekkai-agent`(失敗自動 rollback 到 `kekkai-agent.prev`)
542+
7. cli 或 kekkai.sh 有變 → 個別覆寫,不 restart service
543+
8. 最後印藍色 `UPDATED`(有變)或綠色 `ALREADY UP-TO-DATE`(全部沒變)結果區塊
544544

545545
### 7.7 開發者本機建置(maintainer 用)
546546

@@ -594,14 +594,15 @@ journalctl -u kekkai-agent -n 50 --no-pager
594594
|---|---|
595595
| `interface.name is required` | config 少 `interface.name` 欄位 |
596596
| `lookup iface eth0: ...` | 網卡名錯誤,用 `ip -br link` 查 |
597-
| `this would lock SSH out` | `private.tcp` 有 22 但 `ingress_allowlist` 空,補上你的管理網段 |
598-
| `filter.public.tcp contains 22 but security.allow_ssh_public is false` | 把 22 搬到 `private.tcp`,或 `security.allow_ssh_public: true` |
599-
| `unknown field` | config 有拼錯的欄位(`KnownFields(true)` 嚴格檢查) |
597+
| `port 22 appears in both filter.public.tcp and filter.private.tcp` | 22 只能在其中一個 group,移除其一(或交給 `allow_ssh_public` 自動歸位) |
598+
| `filter.private.tcp contains 22 but security.allow_ssh_public is true` | 22 從 private 移除(會自動進 public),或把 `allow_ssh_public` 改 false |
599+
| `filter.public.tcp contains 22 but security.allow_ssh_public is false` | 22 從 public 移除(會自動進 private),或把 `allow_ssh_public` 改 true |
600+
| `unknown field` | config 有拼錯的欄位(`KnownFields(true)` 嚴格檢查;舊版 `enforce_ssh_private` 會在 migration 時自動移除,不會報這個) |
600601
| `attach xdp: ...` | kernel 不支援 XDP、網卡 driver 不合、或 `CAP_BPF` 不夠 |
601602

602603
### 9.2 SSH 連不上
603604

604-
先確認你的來源 IP 在 `ingress_allowlist` 範圍內:
605+
先確認 22 在哪個 group:`allow_ssh_public: true`(預設)時 22 在 `public.tcp`,任何來源都能連,連不上多半是別的問題(XDP 沒 attach、走錯介面、`static_blocklist` 命中)。若你設成 `false`,22 在 `private.tcp`,要先確認來源 IP 在 `ingress_allowlist` 範圍內:
605606

606607
```bash
607608
who # 目前 SSH session 來源

bpf/xdp_filter.c

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
// non-IPv4/ARP → DROP
88
// 2. IP frag 2+ → PASS (no L4 header to inspect)
99
// 3. conntrack hit → PASS (TCP/UDP stateful fast path)
10-
// 4. return traffic → PASS (TCP ACK, UDP ephemeral, ICMP-if-enabled)
11-
// 4. static blocklist → DROP
12-
// 5. dynamic blocklist → DROP (if not expired)
13-
// 6. public port → PASS (any source)
14-
// 7. private port + allow → PASS
15-
// 8. private port, no allow→ DROP
16-
// 9. no rule → DROP (default deny)
10+
// 4. return traffic → PASS (TCP established, DHCP client, ICMP-if-enabled)
11+
// NOTE: UDP ephemeral-port fallback was removed
12+
// in build.21 — attackers were trivially bypassing
13+
// it by targeting dport >= 32768. UDP return now
14+
// depends entirely on the egress seed (TC hook)
15+
// populating flowtrack_v4, which the stateful
16+
// fast path in step 3 consumes.
17+
// 5. static blocklist → DROP
18+
// 6. dynamic blocklist → DROP (if not expired)
19+
// 7. public port → PASS (any source)
20+
// 8. private port + allow → PASS
21+
// 9. private port, no allow→ DROP
22+
// 10. no rule → DROP (default deny)
1723
//
1824
// All counters are per-CPU to avoid contention on the hot path. Userspace
1925
// sums across CPUs when rendering stats.
@@ -37,7 +43,6 @@ char __license[] SEC("license") = "GPL";
3743
#define EVENTS_RINGBUF_BYTES (1 << 18)
3844
#define FLOW_TCP_TTL_NS (5ULL * 60ULL * 1000000000ULL)
3945
#define FLOW_UDP_TTL_NS (120ULL * 1000000000ULL)
40-
#define DEFAULT_UDP_EPHEMERAL_MIN 32768
4146
#define LEGACY_DHCP_CLIENT_PORT 68
4247

4348
// --- global stats slots -----------------------------------------------------
@@ -270,11 +275,9 @@ static __always_inline int port_in(void *map, __u16 port_be) {
270275
return bpf_map_lookup_elem(map, &port_be) != NULL;
271276
}
272277

273-
static __always_inline void runtime_cfg_get(__u8 *allow_arp, __u8 *allow_icmp,
274-
__u16 *udp_ephemeral_min) {
278+
static __always_inline void runtime_cfg_get(__u8 *allow_arp, __u8 *allow_icmp) {
275279
*allow_arp = 1;
276280
*allow_icmp = 1;
277-
*udp_ephemeral_min = DEFAULT_UDP_EPHEMERAL_MIN;
278281

279282
__u32 k = 0;
280283
struct runtime_cfg_v4 *cfg = bpf_map_lookup_elem(&runtime_cfg_v4, &k);
@@ -283,8 +286,10 @@ static __always_inline void runtime_cfg_get(__u8 *allow_arp, __u8 *allow_icmp,
283286

284287
*allow_arp = (cfg->flags & RUNTIME_FLAG_ALLOW_ARP) ? 1 : 0;
285288
*allow_icmp = (cfg->flags & RUNTIME_FLAG_ALLOW_ICMP) ? 1 : 0;
286-
if (cfg->udp_ephemeral_min >= 1024)
287-
*udp_ephemeral_min = cfg->udp_ephemeral_min;
289+
// cfg->udp_ephemeral_min is deliberately ignored: UDP return traffic
290+
// is now governed entirely by the egress seed + stateful fast path.
291+
// The config field is still accepted by the loader for backward
292+
// compatibility but has no effect on packet decisions.
288293
}
289294

290295
static __always_inline __u64 flow_ttl_ns(__u8 proto) {
@@ -366,8 +371,7 @@ int kekkai_xdp(struct xdp_md *ctx) {
366371
stat_add(STAT_BYTES_TOTAL, pkt_len);
367372

368373
__u8 allow_arp = 1, allow_icmp = 1;
369-
__u16 udp_ephemeral_min = DEFAULT_UDP_EPHEMERAL_MIN;
370-
runtime_cfg_get(&allow_arp, &allow_icmp, &udp_ephemeral_min);
374+
runtime_cfg_get(&allow_arp, &allow_icmp);
371375

372376
// 1. ethernet + IPv4 only (ARP may be toggled at runtime)
373377
struct ethhdr *eth = data;
@@ -462,6 +466,9 @@ int kekkai_xdp(struct xdp_md *ctx) {
462466
}
463467

464468
// 3. stateful conntrack fast path (TCP/UDP only).
469+
// Counters are split: stateful hits bump only the stateful counter,
470+
// not `return`. Earlier builds double-counted here, which made the
471+
// return-traffic numbers unreadable when the box is under load.
465472
if (proto == IPPROTO_TCP || proto == IPPROTO_UDP) {
466473
struct flow4_key fkey = {
467474
.saddr = saddr,
@@ -473,10 +480,8 @@ int kekkai_xdp(struct xdp_md *ctx) {
473480
if (flow_lookup_alive(&fkey, now_ns)) {
474481
if (proto == IPPROTO_TCP) {
475482
stat_add(STAT_PASS_STATEFUL_TCP, 1);
476-
stat_add(STAT_PASS_RETURN_TCP, 1);
477483
} else {
478484
stat_add(STAT_PASS_STATEFUL_UDP, 1);
479-
stat_add(STAT_PASS_RETURN_UDP, 1);
480485
}
481486
stat_add(STAT_PKTS_PASSED, 1);
482487
perip_touch(saddr, pkt_len, proto, 0);
@@ -494,10 +499,12 @@ int kekkai_xdp(struct xdp_md *ctx) {
494499
// Matching RST/FIN is essential — otherwise the server-side
495500
// session dies the moment the peer sends a reset or starts a
496501
// graceful close, because those packets carry no payload ACK.
497-
// - UDP: dst port in ephemeral range (matches responses to
498-
// agent-initiated DNS/NTP/etc without conntrack). DHCP client
499-
// replies (dst 68) are also always allowed so lease renewal does
500-
// not flap the interface IP.
502+
// - UDP: only DHCP client replies (dst 68) are allowed here, so
503+
// lease renewal does not flap the interface IP. All other UDP
504+
// return traffic must come through the stateful fast path above,
505+
// which is fed by the TC egress seed. This is intentional: the
506+
// old "dport >= ephemeral" heuristic was a trivial UDP amplifier
507+
// bypass (attackers just aimed at dport 32768+).
501508
if (proto == IPPROTO_ICMP && allow_icmp) {
502509
stat_add(STAT_PASS_RETURN_ICMP, 1);
503510
stat_add(STAT_PKTS_PASSED, 1);
@@ -528,17 +535,10 @@ int kekkai_xdp(struct xdp_md *ctx) {
528535
perip_touch(saddr, pkt_len, proto, 0);
529536
return XDP_PASS;
530537
}
531-
if (bpf_ntohs(dport_be) >= udp_ephemeral_min) {
532-
struct flow4_key fkey = {
533-
.saddr = saddr, .daddr = ip->daddr,
534-
.sport = sport_be, .dport = dport_be, .proto = proto,
535-
};
536-
flow_upsert(&fkey, now_ns);
537-
stat_add(STAT_PASS_RETURN_UDP, 1);
538-
stat_add(STAT_PKTS_PASSED, 1);
539-
perip_touch(saddr, pkt_len, proto, 0);
540-
return XDP_PASS;
541-
}
538+
// No UDP ephemeral fallback — the stateful fast path already covers
539+
// legitimate return traffic via the TC egress seed. Fall through to
540+
// the port-policy checks below; anything not matching public/private
541+
// rules will hit default deny.
542542
} else {
543543
// Unknown L4 proto and not a return classification — default deny.
544544
stat_add(STAT_DROP_NO_POLICY, 1);

0 commit comments

Comments
 (0)