Skip to content

Commit 346ed3f

Browse files
author
gitlayzer
committed
feat: add HTTPS policy audit and rotation controls
1 parent 18f5a8d commit 346ed3f

28 files changed

Lines changed: 1848 additions & 65 deletions

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Sealtun 是一款功能强大、设计优雅的 CLI 工具,旨在为 **Sealos
1313
- 👤 **Profile 多账号管理**:可把不同 Sealos 账号、region、workspace 和 kubeconfig 保存为命名 profile,按需切换。
1414
- 🚀 **一键暴露服务**:执行 `sealtun expose 8080`,即可获得一个受信任的 HTTPS URL,将流量安全地路由到本地。
1515
- 🌐 **自定义域名自动化**:可用 `domain plan/add/verify/status/doctor` 生成 CNAME 指引、等待 DNS、绑定域名并检查证书状态。
16-
- 🔗 **临时分享链接**:可用 `share create/list/revoke` 为 HTTPS 隧道生成 1 小时、2 小时等自动失效的访问链接。
16+
- 🔗 **临时分享链接与轮换**:可用 `share create/list/revoke/rotate` 为 HTTPS 隧道生成、废弃或轮换自动失效的访问链接。
17+
- 🛡️ **安全运营**:HTTPS 隧道支持 Basic Auth、Bearer Token、临时链接、IP 规则、rate limit、访问审计和 server secret 轮换。
1718
- 📊 **状态、诊断与工作台**`doctor <tunnel-id>``inspect --remote``logs``events``metrics``dashboard` 可定位本地端口、daemon、远端 Pod、Service、Ingress 与证书问题,也可在本地工作台中管理隧道。
1819
- 🧭 **引导与自动修复**`init` 可根据登录状态和本地监听端口推荐命令/YAML;`resources``watch``doctor --fix --dry-run` 可帮助理解和保守修复隧道状态。
1920
- 🧩 **协议模板**`template https|ssh|tcp|mysql|postgres|redis|mqtt` 可生成直接命令和 `sealtun.yaml` 示例。
@@ -207,9 +208,12 @@ sealtun expose 3000 --ip-allowlist 203.0.113.10,198.51.100.0/24 --ip-denylist 19
207208
# 临时访问链接,默认 1 小时后失效
208209
export SEALTUN_TEMP_TOKEN='review-link-secret'
209210
sealtun expose 3000 --temporary-access-token-env SEALTUN_TEMP_TOKEN --temporary-access-ttl 1h
211+
212+
# 限流和访问审计
213+
sealtun expose 3000 --rate-limit 60/m --audit
210214
```
211215

212-
Bearer Token 和临时链接 token 至少需要 8 个字符,只保存 SHA-256 hash,不会写入 Deployment 参数;临时链接使用 `?_sealtun_token=...` 访问,Sealtun 会在转发到本地服务前移除该查询参数。IP 规则优先使用 Ingress/代理传入的 `X-Real-IP`,再回退到 `X-Forwarded-For` 中最后一个有效的代理确认客户端 IP。Basic Auth 与 Bearer/临时链接同时配置时,任一认证方式通过即可访问。
216+
Bearer Token 和临时链接 token 至少需要 8 个字符,只保存 SHA-256 hash,不会写入 Deployment 参数;临时链接使用 `?_sealtun_token=...` 访问,Sealtun 会在转发到本地服务前移除该查询参数。IP 规则优先使用 Ingress/代理传入的 `X-Real-IP`,再回退到 `X-Forwarded-For` 中最后一个有效的代理确认客户端 IP。Basic Auth 与 Bearer/临时链接同时配置时,任一认证方式通过即可访问。`--rate-limit` 使用固定窗口格式,例如 `60/m``1000/h`;访问审计只记录 allow/deny 原因、状态码、路径和客户端 IP,不记录 token 明文、Authorization header 或 Basic Auth 密码。
213217

214218
为已有 HTTPS 隧道创建、查看和撤销临时分享链接:
215219
```bash
@@ -219,12 +223,34 @@ sealtun share create <tunnel-id> --name review --ttl 1h
219223
# 查看链接元数据,不会泄漏 token 明文
220224
sealtun share list <tunnel-id>
221225

226+
# 轮换指定链接,旧 token 立即失效,新 URL 只显示一次
227+
sealtun share rotate <tunnel-id> review --ttl 1h
228+
222229
# 撤销指定名称的分享链接
223230
sealtun share revoke <tunnel-id> review
224231
```
225232

226233
`share` 只适用于 HTTPS 隧道。SSH/TCP 四层入口没有 HTTP query token 认证层,因此不支持临时分享链接。
227234

235+
查看和更新 HTTPS 访问策略:
236+
```bash
237+
sealtun policy show <tunnel-id>
238+
sealtun policy set <tunnel-id> --rate-limit 60/m --audit
239+
sealtun policy set <tunnel-id> --clear-rate-limit
240+
sealtun policy set <tunnel-id> --no-audit
241+
242+
# 查看最近 10 分钟访问审计
243+
sealtun policy audit <tunnel-id> --since 10m
244+
sealtun policy audit <tunnel-id> --since 10m --json
245+
```
246+
247+
轮换隧道 server secret:
248+
```bash
249+
sealtun rotate <tunnel-id> --server-secret
250+
```
251+
252+
新的 server secret 只在本次命令输出中显示一次,并会写回本地 session;远端 Deployment 会滚动到新 secret。`policy``share``rotate` 都只对当前本地 session 记录对应的隧道生效,HTTPS 访问策略不会应用到 SSH/TCP NodePort 流量。
253+
228254
Sealtun 会自动执行以下操作:
229255
1. 在你的 Sealos Namespace 中启动一个隧道代理 Pod。
230256
2. 配置 Ingress 路由规则。
@@ -396,7 +422,7 @@ sealtun dashboard --addr 127.0.0.1 --port 19777
396422
sealtun dashboard --open
397423
```
398424

399-
Dashboard 默认仅监听本地地址,数据来自当前 active profile/region/namespace 的本地 session、登录状态、远端诊断和自定义域名状态。页面可以创建 HTTPS/SSH/TCP 隧道、执行 `sealtun.yaml` 的 dry-run/diff/apply、stop/start/cleanup 隧道、查看 logs/metrics/events/resources,并执行 domain plan/add/verify/clear。写操作确认前会展示对应 CLI 命令,方便理解 UI 操作等价于哪条 `sealtun` 命令。
425+
Dashboard 默认仅监听本地地址,数据来自当前 active profile/region/namespace 的本地 session、登录状态、远端诊断和自定义域名状态。页面可以创建 HTTPS/SSH/TCP 隧道、执行 `sealtun.yaml` 的 dry-run/diff/apply、stop/start/cleanup 隧道、查看 logs/metrics/events/resources/audit,并执行 domain plan/add/verify/clear、policy set、share rotate 和 server secret rotate。写操作确认前会展示对应 CLI 命令,方便理解 UI 操作等价于哪条 `sealtun` 命令。
400426

401427
Dashboard 会优先通过实时连接刷新状态,顶部显示 `Live``Reconnecting``Polling``Disconnected`;实时连接失败时自动回退到 15 秒 polling。`Resources` tab 会展示当前隧道的 Deployment、Pod、HTTP Service、TCP NodePort Service、Ingress、Certificate、Issuer 和 Secret 摘要。这里的资源可见性只提示当前 Sealos/Kubernetes 资源占用,例如副本数、Pod 数、Service 类型、NodePort、Ingress host 数和证书是否存在,不做云账单金额估算;Secret 只展示名称、类型和元数据,不展示 data。`New Tunnel` 面板也可以通过 `Discover local ports` 扫描本机 TCP listening 端口并预填协议、名称和 localPort。
402428

@@ -437,6 +463,9 @@ tunnels:
437463
credential: admin:change-me
438464
accessPolicy:
439465
bearerTokenEnv: SEALTUN_BEARER_TOKEN
466+
rateLimit: 60/m
467+
audit:
468+
enabled: true
440469
ipAllowlist:
441470
- 203.0.113.10
442471
- 198.51.100.0/24

README_EN.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ It connects your local development machine straight to the internet by dynamical
1313
- 👤 **Named Profiles**: Save different Sealos accounts, regions, workspaces, and kubeconfigs as named profiles and switch between them.
1414
- 🚀 **One-Command Expose**: Execute `sealtun expose 8080`, and get a fully trusted HTTPS URL for your localhost securely routed.
1515
- 🌐 **Custom Domain Automation**: Use `domain plan/add/verify/status/doctor` to generate CNAME guidance, wait for DNS, attach domains, and inspect certificate readiness.
16-
- 🔗 **Temporary Share Links**: Use `share create/list/revoke` to generate expiring links for HTTPS tunnels.
16+
- 🔗 **Temporary Share Links and Rotation**: Use `share create/list/revoke/rotate` to generate, revoke, or rotate expiring links for HTTPS tunnels.
17+
- 🛡️ **Security Operations**: HTTPS tunnels support Basic Auth, Bearer tokens, temporary links, IP rules, rate limits, access audit, and server secret rotation.
1718
- 📊 **Status, Diagnostics, and Workbench**: Use `doctor <tunnel-id>`, `inspect --remote`, `logs`, `events`, `metrics`, and `dashboard` to diagnose local ports, daemon state, remote Pods, Services, Ingresses, and certificates, or manage tunnels from the local workbench.
1819
- 🧭 **Guided UX and Safe Fixes**: Use `init` for first-run command/YAML recommendations, and `resources`, `watch`, or `doctor --fix --dry-run` to understand and conservatively repair tunnel state.
1920
- 🧩 **Protocol Templates**: Use `template https|ssh|tcp|mysql|postgres|redis|mqtt` to generate commands and `sealtun.yaml` examples.
@@ -207,9 +208,12 @@ sealtun expose 3000 --ip-allowlist 203.0.113.10,198.51.100.0/24 --ip-denylist 19
207208
# Temporary access link, expiring after 1 hour by default
208209
export SEALTUN_TEMP_TOKEN='review-link-secret'
209210
sealtun expose 3000 --temporary-access-token-env SEALTUN_TEMP_TOKEN --temporary-access-ttl 1h
211+
212+
# Rate limit and access audit
213+
sealtun expose 3000 --rate-limit 60/m --audit
210214
```
211215

212-
Bearer and temporary-link tokens must be at least 8 characters. They are stored only as SHA-256 hashes and are not written into Deployment args. Temporary links use `?_sealtun_token=...`; Sealtun strips that query parameter before forwarding the request to your local service. IP rules prefer the `X-Real-IP` value set by the Ingress/proxy and fall back to the last valid proxy-confirmed client IP in `X-Forwarded-For`. When Basic Auth and Bearer/temporary tokens are both configured, either authentication method can grant access.
216+
Bearer and temporary-link tokens must be at least 8 characters. They are stored only as SHA-256 hashes and are not written into Deployment args. Temporary links use `?_sealtun_token=...`; Sealtun strips that query parameter before forwarding the request to your local service. IP rules prefer the `X-Real-IP` value set by the Ingress/proxy and fall back to the last valid proxy-confirmed client IP in `X-Forwarded-For`. When Basic Auth and Bearer/temporary tokens are both configured, either authentication method can grant access. `--rate-limit` uses fixed-window specs such as `60/m` or `1000/h`; access audit records only allow/deny reason, status, path, and client IP, never plaintext tokens, Authorization headers, or Basic Auth passwords.
213217

214218
Create, list, and revoke temporary share links for an existing HTTPS tunnel:
215219
```bash
@@ -219,12 +223,34 @@ sealtun share create <tunnel-id> --name review --ttl 1h
219223
# List metadata without revealing tokens
220224
sealtun share list <tunnel-id>
221225

226+
# Rotate a named link. The old token is invalidated and the new URL is shown once.
227+
sealtun share rotate <tunnel-id> review --ttl 1h
228+
222229
# Revoke a named share link
223230
sealtun share revoke <tunnel-id> review
224231
```
225232

226233
`share` only applies to HTTPS tunnels. SSH/TCP L4 entries do not have an HTTP query-token layer and therefore do not support temporary share links.
227234

235+
Show and update HTTPS access policy:
236+
```bash
237+
sealtun policy show <tunnel-id>
238+
sealtun policy set <tunnel-id> --rate-limit 60/m --audit
239+
sealtun policy set <tunnel-id> --clear-rate-limit
240+
sealtun policy set <tunnel-id> --no-audit
241+
242+
# Show the last 10 minutes of access audit events
243+
sealtun policy audit <tunnel-id> --since 10m
244+
sealtun policy audit <tunnel-id> --since 10m --json
245+
```
246+
247+
Rotate the tunnel server secret:
248+
```bash
249+
sealtun rotate <tunnel-id> --server-secret
250+
```
251+
252+
The new server secret is printed only once and saved back to the local session; the remote Deployment rolls to the new secret. `policy`, `share`, and `rotate` operate on the tunnel represented by the local session. HTTPS access policies do not apply to SSH/TCP NodePort traffic.
253+
228254
Sealtun will:
229255
1. Spin up a tunnel proxy Pod in your Sealos namespace.
230256
2. Establish the Ingress routes.
@@ -396,7 +422,7 @@ sealtun dashboard --addr 127.0.0.1 --port 19777
396422
sealtun dashboard --open
397423
```
398424

399-
The dashboard listens locally by default and uses only the current active profile/region/namespace. It reads local sessions, login state, remote diagnostics, and custom domain readiness. The page can create HTTPS/SSH/TCP tunnels, run `sealtun.yaml` dry-run/diff/apply, stop/start/cleanup tunnels, view logs/metrics/events/resources, and run domain plan/add/verify/clear. Before write confirmations, it previews the equivalent CLI command so the UI operation is not a black box.
425+
The dashboard listens locally by default and uses only the current active profile/region/namespace. It reads local sessions, login state, remote diagnostics, and custom domain readiness. The page can create HTTPS/SSH/TCP tunnels, run `sealtun.yaml` dry-run/diff/apply, stop/start/cleanup tunnels, view logs/metrics/events/resources/audit, and run domain plan/add/verify/clear, policy set, share rotate, and server secret rotate. Before write confirmations, it previews the equivalent CLI command so the UI operation is not a black box.
400426

401427
The dashboard prefers live status updates and shows `Live`, `Reconnecting`, `Polling`, or `Disconnected` in the top bar; if the live stream fails it falls back to 15-second polling. The `Resources` tab shows the tunnel's Deployment, Pods, HTTP Service, TCP NodePort Service, Ingress, Certificate, Issuer, and Secret summaries. Resource visibility is not cloud billing estimation; it only highlights current Sealos/Kubernetes occupancy such as replica count, Pod count, Service type, NodePort, Ingress host count, and certificate presence. Secrets expose only name, type, and metadata, never data. The `New Tunnel` panel can also run `Discover local ports` to scan local TCP listening ports and prefill protocol, name, and localPort.
402428

@@ -437,6 +463,9 @@ tunnels:
437463
credential: admin:change-me
438464
accessPolicy:
439465
bearerTokenEnv: SEALTUN_BEARER_TOKEN
466+
rateLimit: 60/m
467+
audit:
468+
enabled: true
440469
ipAllowlist:
441470
- 203.0.113.10
442471
- 198.51.100.0/24

cmd/access_policy.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type accessPolicyInput struct {
2020
TemporaryTokenEnv string
2121
TemporaryTTL time.Duration
2222
TemporaryName string
23+
RateLimit string
24+
AuditEnabled bool
2325
}
2426

2527
type applyAccessPolicy struct {
@@ -28,6 +30,8 @@ type applyAccessPolicy struct {
2830
IPAllowlist []string `json:"ipAllowlist,omitempty" yaml:"ipAllowlist,omitempty"`
2931
IPDenylist []string `json:"ipDenylist,omitempty" yaml:"ipDenylist,omitempty"`
3032
TemporaryLinks []applyTemporaryLink `json:"temporaryLinks,omitempty" yaml:"temporaryLinks,omitempty"`
33+
RateLimit string `json:"rateLimit,omitempty" yaml:"rateLimit,omitempty"`
34+
Audit *applyAuditConfig `json:"audit,omitempty" yaml:"audit,omitempty"`
3135
}
3236

3337
type applyTemporaryLink struct {
@@ -38,6 +42,10 @@ type applyTemporaryLink struct {
3842
ExpiresAt string `json:"expiresAt,omitempty" yaml:"expiresAt,omitempty"`
3943
}
4044

45+
type applyAuditConfig struct {
46+
Enabled bool `json:"enabled" yaml:"enabled"`
47+
}
48+
4149
func resolveAccessPolicy(input accessPolicyInput, now time.Time, lookupEnv func(string) string) (*session.AccessPolicy, error) {
4250
policy := &session.AccessPolicy{}
4351
if input.BearerToken != "" || input.BearerTokenEnv != "" {
@@ -53,6 +61,10 @@ func resolveAccessPolicy(input accessPolicyInput, now time.Time, lookupEnv func(
5361
}
5462
policy.IPAllowlist = normalizeStringList(input.IPAllowlist)
5563
policy.IPDenylist = normalizeStringList(input.IPDenylist)
64+
policy.RateLimit = strings.TrimSpace(input.RateLimit)
65+
if input.AuditEnabled {
66+
policy.Audit = &session.AuditConfig{Enabled: true}
67+
}
5668
if input.TemporaryToken != "" || input.TemporaryTokenEnv != "" {
5769
if input.TemporaryTTL <= 0 {
5870
return nil, fmt.Errorf("temporary access token requires --temporary-access-ttl greater than 0")
@@ -88,6 +100,10 @@ func resolveApplyAccessPolicy(config *applyAccessPolicy, now time.Time, lookupEn
88100
policy := &session.AccessPolicy{
89101
IPAllowlist: normalizeStringList(config.IPAllowlist),
90102
IPDenylist: normalizeStringList(config.IPDenylist),
103+
RateLimit: strings.TrimSpace(config.RateLimit),
104+
}
105+
if config.Audit != nil && config.Audit.Enabled {
106+
policy.Audit = &session.AuditConfig{Enabled: true}
91107
}
92108
if config.BearerToken != "" || config.BearerTokenEnv != "" {
93109
token, err := resolveSecretValue(config.BearerToken, config.BearerTokenEnv, "bearer token", lookupEnv)
@@ -178,7 +194,16 @@ func accessPolicyToRuntime(policy *session.AccessPolicy) *accesspolicy.Policy {
178194
IPAllowlist: append([]string(nil), policy.IPAllowlist...),
179195
IPDenylist: append([]string(nil), policy.IPDenylist...),
180196
TemporaryTokens: tokens,
197+
RateLimit: policy.RateLimit,
198+
Audit: auditConfigToRuntime(policy.Audit),
199+
}
200+
}
201+
202+
func auditConfigToRuntime(config *session.AuditConfig) *accesspolicy.AuditConfig {
203+
if config == nil {
204+
return nil
181205
}
206+
return &accesspolicy.AuditConfig{Enabled: config.Enabled}
182207
}
183208

184209
func resolveSecretValue(value, envName, label string, lookupEnv func(string) string) (string, error) {

cmd/apply_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ func TestNormalizeApplyTunnelResolvesAccessPolicyAndTTL(t *testing.T) {
275275
BearerTokenEnv: "SEALTUN_TEST_BEARER",
276276
IPAllowlist: []string{"10.0.0.0/8"},
277277
IPDenylist: []string{"10.0.0.9"},
278+
RateLimit: "60/m",
279+
Audit: &applyAuditConfig{Enabled: true},
278280
TemporaryLinks: []applyTemporaryLink{{
279281
Name: "review",
280282
TokenEnv: "SEALTUN_TEST_TEMP",
@@ -294,6 +296,9 @@ func TestNormalizeApplyTunnelResolvesAccessPolicyAndTTL(t *testing.T) {
294296
if len(normalized.AccessPolicy.TemporaryTokens) != 1 || normalized.AccessPolicy.TemporaryTokens[0].Name != "review" {
295297
t.Fatalf("expected temporary link config, got %#v", normalized.AccessPolicy.TemporaryTokens)
296298
}
299+
if normalized.AccessPolicy.RateLimit != "60/m" || normalized.AccessPolicy.Audit == nil || !normalized.AccessPolicy.Audit.Enabled {
300+
t.Fatalf("expected rate limit and audit config, got %#v", normalized.AccessPolicy)
301+
}
297302
if normalized.ExpiresAt == "" {
298303
t.Fatal("expected ttl to produce expiresAt")
299304
}

0 commit comments

Comments
 (0)