hectorm/cardea
GitHub: hectorm/cardea
一个轻量级 SSH 堡垒机服务器,通过文本文件管理访问规则并提供会话录制与 TPM 密钥保护。
Stars: 27 | Forks: 2
# Cardea
Cardea 是一个 SSH 堡垒机服务器,具备访问控制、会话记录以及可选的基于 TPM 的密钥保护功能。
## 范围
Cardea 专为通过代码管理基础设施的中小型团队设计。访问规则存放在文本文件中,因此可以在 pull request 中进行审查,并像其他任何配置一样进行版本控制。它可以直接开箱即用,也可以作为更大型系统的构建模块,用于从外部事实来源生成配置。不需要数据库或 Web UI。
## 工作原理
客户端使用任何标准 SSH 客户端进行连接,并在 SSH 用户名中编码目标后端(例如:`user@backend@bastion`,[查看格式](#client-connection))。堡垒机会对客户端进行身份验证,核实访问规则,并使用自身的密钥连接到后端。可以选择以 [asciinema v3](https://www.asciinema.org) 格式记录会话。
```
sequenceDiagram
participant C as Client
participant B as Bastion (Cardea)
participant S as Backend Server
C->>B: SSH connect (user@backend@bastion)
B->>B: Validate client public key
B->>B: Check access rules (permitconnect)
B->>S: SSH connect (bastion's key)
S-->>B: Session established
B-->>C: Session established
C->>B: Commands / Data
B->>S: Forward commands
B->>B: Record session (optional)
S-->>B: Response
B-->>C: Forward response
```
## 客户端连接
为了进行连接,客户端需在 SSH 用户名中指定他们希望访问的后端服务器。支持以下格式:
```
# 使用 @ 和 : 作为分隔符
ssh -p
@[:]@
ssh -p -o User=@[:]
# 使用 + 作为分隔符(以避免与 SSH 使用的 @ 产生歧义)
ssh -p +[+]@
ssh -p -o User=+[+]
```
### 示例
```
ssh -p 2222 alice@10.0.1.1@cardea.internal
ssh -p 2222 -o User=alice@10.0.1.1 cardea.internal
ssh -p 2222 alice+10.0.1.1@cardea.internal
ssh -p 2222 -o User=alice+10.0.1.1 cardea.internal
# 使用 SSH config 文件
cat >> ~/.ssh/config <<-'EOF'
Host backend
HostName cardea.internal
Port 2222
User alice@10.0.1.1
EOF
ssh backend
# 使用 sftp
sftp -P 2222 alice+10.0.1.1@cardea.internal
sftp -P 2222 -o User=alice@10.0.1.1 cardea.internal
# 使用 rsync
rsync -ave 'ssh -p 2222' alice+10.0.1.1@cardea.internal:/remote/dir/ /local/dir/
rsync -ave 'ssh -p 2222 -o User=alice@10.0.1.1' cardea.internal:/remote/dir/ /local/dir/
```
## 安装
### Docker
```
docker run -p '2222:2222' -u "$(id -u):$(id -g)" --mount 'type=bind,src=./data/,dst=/data/' ghcr.io/hectorm/cardea:v1
```
镜像可在 [GitHub Container Registry](https://github.com/hectorm/cardea/pkgs/container/cardea) 和 [Docker Hub](https://hub.docker.com/r/hectorm/cardea) 上获取。
### 预编译二进制文件
从 [发布页面](https://github.com/hectorm/cardea/releases) 下载。构建是可重现且不可变的,并包含 [来源证明](https://docs.github.com/en/actions/concepts/security/artifact-attestations)。
## 配置
### 命令行选项
```
-listen string
address for the SSH server (env CARDEA_LISTEN) (default ":2222")
-health-listen string
address for the health/metrics server; disabled if empty (env CARDEA_HEALTH_LISTEN) (default "localhost:9222")
-node-id string
stable identifier for the instance; expands ${VAR} from the environment (env CARDEA_NODE_ID)
-key-strategy string
key strategy for bastion host/backend authentication: file, tpm (env CARDEA_KEY_STRATEGY) (default "file")
-private-key-file string
path to the host private key (env CARDEA_PRIVATE_KEY_FILE) (default "/etc/cardea/private_key")
-private-key-passphrase string
passphrase for the private key (env CARDEA_PRIVATE_KEY_PASSPHRASE)
-private-key-passphrase-file string
path to the file containing the private key passphrase (env CARDEA_PRIVATE_KEY_PASSPHRASE_FILE)
-tpm-device string
path to the TPM device (env CARDEA_TPM_DEVICE) (default "/dev/tpmrm0")
-tpm-parent-handle string
persistent handle for the parent key (e.g. 0x81000001); if not set, a transient key is created (env CARDEA_TPM_PARENT_HANDLE)
-tpm-parent-auth string
authorization value for the parent key (env CARDEA_TPM_PARENT_AUTH)
-tpm-parent-auth-file string
path to the file containing the parent key authorization (env CARDEA_TPM_PARENT_AUTH_FILE)
-tpm-key-file string
path to the key blob (env CARDEA_TPM_KEY_FILE) (default "/etc/cardea/tpm_key.blob")
-tpm-key-auth string
authorization value for the key (env CARDEA_TPM_KEY_AUTH)
-tpm-key-auth-file string
path to the file containing the key authorization (env CARDEA_TPM_KEY_AUTH_FILE)
-authorized-keys-file string
path to the authorized keys file (env CARDEA_AUTHORIZED_KEYS_FILE) (default "/etc/cardea/authorized_keys")
-known-hosts-file string
path to the known hosts file (env CARDEA_KNOWN_HOSTS_FILE) (default "/etc/cardea/known_hosts")
-unknown-hosts-policy string
policy for unknown hosts: strict (deny unknown), tofu (trust on first use) (env CARDEA_UNKNOWN_HOSTS_POLICY) (default "strict")
-banner-file string
path to the banner file; disabled if empty (env CARDEA_BANNER_FILE)
-connections-max int
maximum number of concurrent connections; 0 for unlimited (env CARDEA_CONNECTIONS_MAX) (default 1000)
-sessions-max int
maximum number of concurrent session channels per connection; 0 for unlimited (env CARDEA_SESSIONS_MAX) (default 10)
-forwards-max int
maximum number of concurrent forwarding channels per connection; 0 for unlimited (env CARDEA_FORWARDS_MAX) (default 1024)
-rate-limit-max int
maximum number of unauthenticated requests per IP address; 0 for unlimited (env CARDEA_RATE_LIMIT_MAX) (default 10)
-rate-limit-time duration
time window for rate limiting unauthenticated requests (env CARDEA_RATE_LIMIT_TIME) (default 5m0s)
-recordings-dir string
path to the session recordings directory; disabled if empty (env CARDEA_RECORDINGS_DIR)
-recordings-retention-time duration
retention time for the session recordings (env CARDEA_RECORDINGS_RETENTION_TIME) (default 720h0m0s)
-recordings-max-disk-usage string
maximum disk usage for session recordings; accepts percentage (e.g. 90%) or fixed size (e.g. 1GB) (env CARDEA_RECORDINGS_MAX_DISK_USAGE) (default "0")
-log-level string
log level: debug, info, warn, error, quiet (env CARDEA_LOG_LEVEL) (default "info")
-validate-authorized-keys
validate the authorized keys file and exit
-validate-known-hosts
validate the known hosts file and exit
-version
show version and exit
```
### 授权密钥格式
Cardea 使用 SSH 授权密钥格式的一种变体来定义每个密钥的访问规则和选项。
```
permitconnect="user1@host1:port1,user2@host2:port2",permitopen="host1:port1,host2:port2",command="cmd",no-pty,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
```
#### 必需项
- **`permitconnect`**:允许的后端服务器连接列表(以逗号分隔,可多次指定)。
- **格式:**`@[:]` 或 `+[+]`,其中 `` 是后端服务器用户名。
- 支持对用户使用 glob 模式(由 [Go `filepath.Match` 函数](https://pkg.go.dev/path/filepath#Match) 定义)。
- 支持对主机使用 glob 模式和 CIDR 块。
- 支持精确端口、范围(例如 `8000-8999`)以及匹配任何端口的 `*`。
- 如果未指定端口,则使用默认的 SSH 端口 (22)。
- 如果同一个公钥存在多个 `permitconnect` 选项,则使用第一个匹配项,并应用该匹配中指定的选项。
- **示例:**`permitconnect="alice@*.internal,alice@10.0.0.0/16"`。
#### 可选项
- **`permitopen`**:允许的本地端口转发目标列表(以逗号分隔,可多次指定)。
- **格式:**`:`。
- 支持对主机使用 glob 模式和 CIDR 块。
- 支持精确端口、范围(例如 `8000-8999`)以及匹配任何端口的 `*`。
- 默认情况下,仅允许发往任何端口的 localhost 流量。
- **示例:**`permitopen="localhost:1-65535,127.0.0.1/8:1-65535,[::1/128]:1-65535"`。
- **`permitlisten`**:允许的远程端口转发绑定地址列表(以逗号分隔,可多次指定)。
- **格式:**`:`。
- 支持对主机使用 glob 模式和 CIDR 块。
- 支持精确端口、范围(例如 `8000-8999`)以及匹配任何端口的 `*`。
- 默认情况下,远程端口转发处于禁用状态。
- **示例:**`permitlisten="localhost:8080,0.0.0.0:8000-8999"`。
- **`permitsocketopen`**:允许的本地 Unix socket 转发目标(可多次指定)。
- **格式:**绝对或相对 socket 路径。
- 支持使用 glob 模式。使用单独的 `*` 以允许任何路径。
- 绝对请求仅匹配绝对模式,相对请求仅匹配相对模式。
- 默认情况下,本地 socket 转发处于禁用状态。
- **示例:**`permitsocketopen="/var/run/docker.sock",permitsocketopen="/tmp/*.sock"`。
- **`permitsocketlisten`**:允许的远程 Unix socket 转发绑定路径(可多次指定)。
- **格式:**绝对或相对 socket 路径。
- 支持使用 glob 模式。使用单独的 `*` 以允许任何路径。
- 绝对请求仅匹配绝对模式,相对请求仅匹配相对模式。
- 默认情况下,远程 socket 转发处于禁用状态。
- **示例:**`permitsocketlisten="/tmp/agent.sock"`。
- **`environment`**:控制后端会话上的环境变量(可多次指定)。支持三种形式:
- **`NAME=value`**:设置服务端变量。客户端无法覆盖它。如果对同一个名称多次指定,则以最后一个为准。
- **`+PATTERN`**:允许客户端转发匹配该 glob 模式的变量。
- **`-PATTERN`**:阻止客户端转发匹配该 glob 模式的变量。
- 默认情况下,所有客户端环境变量都会被阻止。规则按顺序求值;最后匹配到的 `+` 或 `-` 规则生效。
- 后端 SSH 服务器必须配置为接受这些变量(例如,OpenSSH `sshd_config` 中的 `AcceptEnv NAME`)。
- **示例:**`environment="LANG=en_US.UTF-8",environment="+TERM",environment="+LC_*",environment="-LC_MESSAGES"`。
- **`from`**:允许使用此密钥的源 IP 模式列表(以逗号分隔,可多次指定)。
- 支持对主机使用 glob 模式和 CIDR 块。
- 支持使用 `!` 前缀进行取反(模式按顺序求值,取反覆盖之前的匹配)。
- **示例:**`from="10.0.0.0/8,!10.0.1.1"`。
- **`start-time`**:在此时间戳之前密钥尚未生效。
- **格式:**`YYYYMMDD[HHMM[SS]][Z]`,其中 `Z` 表示 UTC(如果省略则使用本地时间)。
- 如果指定了多个 `start-time` 选项,则使用时间最靠未来的那个。
- **示例:**`start-time="20060102150405Z"`。
- **`expiry-time`**:在此时间戳之后密钥不再有效。
- **格式:**`YYYYMMDD[HHMM[SS]][Z]`,其中 `Z` 表示 UTC(如果省略则使用本地时间)。
- 如果指定了多个 `expiry-time` 选项,则使用最接近当前时间的那个。
- **示例:**`expiry-time="20060102150405Z"`。
- **`time-window`**:定期出现的基于时间的访问控制窗口(可多次指定)。
- **格式:**`[,...]`,其中每个窗口是一组以空格分隔的约束条件(AND 逻辑),多个窗口以逗号分隔(OR 逻辑)。
- **约束类型:**`dow`(星期几,0–6 或 sun–sat)、`month`(1–12 或 jan–dec)、`day`(1–31)、`hour`(0–23)、`min`(0–59)、`sec`(0–59)、`tz`(IANA 时区)。
- 约束支持单个值(例如 `hour:8`)、包含范围(例如 `hour:8-17`),以及通过 `/` 指定多个不连续的范围(例如 `hour:8-13/15-17`)。
- 省略的约束类型匹配任何值(例如,`hour:8-17` 匹配任何一天的 8–17 点)。
- 不支持跨夜范围(例如,`dow:fri-mon` 无效;应使用 `dow:fri-sun/mon` 代替)。
- 如果省略 `tz:`,则使用系统本地时间。逗号分隔列表中的每个窗口都可以有自己的 `tz:` 值。
- 如果指定了多个 `time-window` 选项,窗口将被累加(OR 逻辑)。
- **示例:**`time-window="dow:mon-fri hour:8-17 tz:Europe/Madrid"`。
- **示例:**`time-window="dow:mon-thu hour:8-17 tz:Europe/Madrid,dow:fri hour:8-14 tz:Europe/Madrid"`。
- **`command`**:强制执行特定命令。
- **示例:**`command="nologin"`。
- **`no-pty`**:禁用伪终端分配。
- **`pty`**:启用伪终端分配(覆盖 `restrict` 或 `no-pty`)。
- **`no-port-forwarding`**:同时禁用本地和远程端口转发。
- **`port-forwarding`**:启用端口转发(覆盖 `restrict` 或 `no-port-forwarding`)。
- **`no-socket-forwarding`**:同时禁用本地和远程 Unix socket 转发。
- **`socket-forwarding`**:启用 socket 转发(覆盖 `restrict` 或 `no-socket-forwarding`)。
- **`restrict`**:启用所有限制(等同于 `no-pty,no-port-forwarding,no-socket-forwarding`)。
- 与 `pty`、`port-forwarding` 或 `socket-forwarding` 配合使用以选择性地重新启用功能。
- **示例:**`restrict,pty,permitconnect="*@*:22"`(允许 PTY,但不允许端口或 socket 转发)。
- **`no-recording`**:禁用此密钥的会话记录。
- **`recording`**:启用会话记录(覆盖 `no-recording`)。
#### 扩展
该格式支持注释、指令、行连接和管道扩展:
- **`#`**:注释(位于行首或行尾)。
- **`\`**:连接下一行(必须紧跟在换行符之前)。
- **`|`**:允许多个密钥共享相同的选项(例如,`permitconnect="..." KEY1 | KEY2 | KEY3`)。
**指令:**
- **`#define (NAME|{{NAME}}) value`**:定义一个宏(`[A-Za-z_][A-Za-z0-9_]*`)。裸名称会在 `NAME` 和 `{{NAME}}` 引用处展开。带括号的名称仅会在 `{{NAME}}` 引用处展开。即使在引号括起来的值内,两者也都会被展开。
**示例:**
```
# === Keys ===
#define ALICE_KEY ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... alice@example.com
#define BOB_KEY ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... bob@example.com
#define CAROL_KEY ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... carol@example.com
#define DAVE_KEY ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... dave@example.com
#define CI_KEY ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... deploy@ci
# === Teams ===
#define SRE_TEAM ALICE_KEY | BOB_KEY
#define DEV_TEAM CAROL_KEY | DAVE_KEY
#define ALL_TEAMS SRE_TEAM | DEV_TEAM
# === Servers ===
#define DEV_SERVERS *@dev.example.com:22
#define STAGING_SERVERS *@staging.example.com:22
#define PROD_SERVERS \
# web server
*@example.com:22, \
# API server
*@api.example.com:22
# === Server groups ===
#define NON_PROD_SERVERS DEV_SERVERS,STAGING_SERVERS
#define ALL_SERVERS NON_PROD_SERVERS,PROD_SERVERS
# === Option templates ===
#define WORKING_HOURS dow:mon-fri hour:8-17 tz:Europe/Madrid
#define SFTP_OPTS command="internal-sftp",restrict
# === Access rules ===
# SRE:对所有 servers 的完全访问权限
permitconnect="ALL_SERVERS",permitopen="*:*" SRE_TEAM
# Developers:仅限非生产环境
permitconnect="DEV_SERVERS" DEV_TEAM
permitconnect="STAGING_SERVERS",time-window="WORKING_HOURS" DEV_TEAM
# CI/CD:仅限通过 SFTP 部署到生产环境
permitconnect="PROD_SERVERS",SFTP_OPTS CI_KEY
# Git:所有人的 repository 访问权限
permitconnect="*@git.example.com:22" ALL_TEAMS | CI_KEY
```
### 已知主机格式
Cardea 使用标准的 OpenSSH 已知主机格式,在堡垒机连接到后端服务器时验证后端服务器的主机密钥。
```
[host]:port ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
```
#### 未知主机策略
`--unknown-hosts-policy` 选项控制 Cardea 如何处理连接到那些主机密钥不存在于已知主机文件中的后端服务器的情况。
- **`strict`(默认):**拒绝连接到未知主机。如果后端服务器的主机密钥不在已知主机文件中或不匹配,则连接失败。
- **`tofu`(首次使用时信任):**在首次连接时,自动将未知主机密钥添加到已知主机文件中。后续连接将根据存储的密钥进行验证。当新主机被信任时,会记录一条包含指纹和公钥的警告日志。
#### 证书颁发机构
Cardea 支持 `@cert-authority` 条目,用于基于 SSH 证书的主机验证,允许后端服务器出示由受信任的 CA 签名的证书,而不需要单独的主机密钥。主机模式支持通配符(例如,`*`、`*.internal`、`*:22`)。
**示例:**
```
# 信任单个 host keys
[10.0.1.1]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
[10.0.1.2]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
# 在端口 22 上为所有 backend servers 信任 CA
@cert-authority *:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
# 为特定 domain 信任 CA
@cert-authority *.internal:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
```
在拥有众多后端服务器的环境中,使用证书颁发机构可以简化主机密钥管理,因为只需分发 CA 公钥,而无需分发单独的主机密钥。
## TPM 模式
Cardea 支持将其私钥存储在 TPM 2.0 模块(`--key-strategy=tpm`)中,从而防止即使服务器被入侵也能提取密钥。
这可以防止通过窃取磁盘、备份或意外泄露而导致的密钥泄露。密钥 blob 绑定到创建它的 TPM 上,不能在其他地方使用。
### 持久化 SRK 配置
默认情况下,Cardea 在每次启动时都会创建一个临时 SRK。对于需要持久化 SRK 的环境,请使用 `tpm2-tools` 进行配置:
```
# 创建匹配 Cardea 的 SRK template 的 primary key(参见 internal/tpm/key.go)
tpm2_createprimary -C o -G ecc256:aes128cfb -g sha256 -c /tmp/srk.ctx -a 'fixedtpm|fixedparent|sensitivedataorigin|userwithauth|noda|restricted|decrypt' # -p file:
# 持久化到 handle 0x81000001
tpm2_evictcontrol -C o -c /tmp/srk.ctx 0x81000001
# 与 Cardea 结合使用
cardea --key-strategy=tpm --tpm-parent-handle=0x81000001 # --tpm-parent-auth-file=
# 移除持久化 handle(如果需要)
tpm2_evictcontrol -C o -c 0x81000001
```
## 许可证
采用 [European Union Public Licence v. 1.2 or later](./LICENSE) © [Héctor Molinero Fernández]() 授权。在使用或分发前请审查许可证条件。标签:EVTX分析, SSH, Streamlit, 会话录制, 内存分配, 基础设施管理, 堡垒机, 日志审计, 访问控制, 请求拦截, 运维