给 Home Assistant 用的 Tesla Fleet API 虚拟钥匙与命令代理,打包成一个开箱即用的 Docker 容器。
围绕 Tesla 官方的 tesla-http-proxy
二进制,补上它没有管的那些事:密钥生成、证书签发、OAuth 向导、公钥发布,
以及告诉你该往 Home Assistant 的四个输入框里填什么。
名字的由来:这个项目真正在做的事不是"代理 HTTP",而是当一个钥匙盒—— 里面锁着让 Fleet API 命令能下发的那把车辆私钥,外加 HA 信任的 TLS 证书和 OAuth token;对外则负责把公钥挂出去、把车配对上、把证书交给 HA。 代理只是这盒钥匙的用法之一。
这也是磁盘布局的设计原则:
/var/lib/tesla-keybox就是那个盒子, 备份它、锁好它,其余全是可以随时重建的派生物。宿主机侧的 CLI 叫
keybox(./scripts/keybox),命令名比仓库名短一截, 和kubectl之于 kubernetes 是一个道理。
Fleet API 代理本身不难跑,难的是它周围那一圈:证书主体名要对得上、公钥要能被 Tesla 从公网抓到、每辆车都要在中控上批准一次、HA 里那四个值到底填什么。 大部分人卡住的地方都在这里,而不是代理本身。
- 配置向导常驻,不是"配完就退出"。随时能重跑,不用重启容器。
- 区域端点是数据不是常量,四条轴(授权页 / token / API / 配对深链)各自独立,
还能被环境变量覆盖——Tesla 半夜改端点时改
.env就行。 - 证书带 SAN,HA 用容器名、主机名还是 IP 连过来都不会报那句语焉不详的 SSL 错误。
scripts/keybox会读你实际的 Docker 拓扑,直接给出 HA 该填的四个值。- CI 会真的启动容器并攻击它:杀进程验自愈、删公钥验重导出、验证书 SAN、 验私钥权限、验中英文界面都能渲染。跑不过不许合并。
cp .env.example .env
$EDITOR .env # 只有四项必填
docker compose up -d然后:
- 把
./public挂给你的反向代理当 webroot,确认这个 URL 返回 200:https://<DOMAIN>/.well-known/appspecific/com.tesla.3p.public-key.pem - 打开
http://<服务器>:8099,按页面上的五步走完 ./scripts/keybox—— 拿到 Home Assistant 要填的四个值
第 1 步不完成,后面每一步都不会成功。向导首页的状态台账会告诉你卡在哪。
反向代理不需要任何 rewrite 规则,因为发布目录就是 URL 路径的镜像:
# Caddy
tesla.example.com {
root * /path/to/public
file_server
}
# nginx
location /.well-known/ { root /path/to/public; }
容器
└── s6-overlay
├── init-config (oneshot) 校验配置,坏了直接让容器退出
├── init-keys (oneshot) GPG/pass、Tesla 密钥对、TLS 证书、发布公钥
├── webui (longrun) :8099 配置向导,常驻
└── proxy (longrun) :443 Tesla 官方 Go 二进制
没有状态机,没有自研的进程管理器,也没有插件框架。
配置向导和代理端口不同、职责不同,本来就能并存——承认这一点之后,
"两个进程互斥、需要一个主控来调度"这个前提就不成立了,围绕它建的东西自然也就
不需要了。进程崩了由 s6 拉起,日志直接进 docker logs。
按 FHS 布置,不用容器圈常见的 /data + /share——那两个名字什么也没说明,
而这里最重要的信息恰恰是哪个目录丢了会要命。分成三块之后,备份策略从目录名
就能读出来。
| 容器内路径 | 内容 | 备份 |
|---|---|---|
/etc/tesla-keybox/ |
regions.json 等只读配置,随镜像走 |
不用 |
/var/lib/tesla-keybox/ |
持久状态——这就是"钥匙盒"本身 | ★ 只需备份这一个 |
/srv/tesla-keybox/ |
对外发布的派生产物 | 不用,可重建 |
/var/lib/tesla-keybox/ ← 唯一需要备份的卷
├── keys/
│ ├── gnupg/ GNUPGHOME (0700)
│ └── store/ pass —— 车辆私钥
├── tls/
│ ├── proxy.crt
│ ├── proxy.key (0600)
│ └── proxy.san SAN 指纹,用于判断是否需要重签
└── oauth/
├── refresh_token (0600)
└── access_token (0600)
/srv/tesla-keybox/
├── public/ 挂给反向代理当 webroot
│ └── .well-known/appspecific/com.tesla.3p.public-key.pem
└── ha/
└── keybox-ca.crt 挂给 Home Assistant(HA 把它当 CA 加载)
./scripts/keybox backup # 导出状态卷
./scripts/keybox restore FILE # 从备份恢复keys/store 损坏是本项目唯一无法自动恢复的故障:私钥没了要重新生成密钥对、
重新注册,并且到每一辆车的中控上重新批准配对。其它一切都可以重来。
config/ → /etc/tesla-keybox 端点表
src/ → /opt/tesla-keybox Flask 向导与区域解析
rootfs/ → / s6 服务定义与 init 脚本
scripts/ 宿主机侧 CLI
tests/ pytest
配置向导默认只绑 127.0.0.1。 这个页面能取到 refresh token,而 refresh token
等同于整个 Tesla 账号——定位、开锁、开后备箱、查历史轨迹,不只是充电。
局域网里也不该裸奔。
需要从别的机器配置时用 SSH 端口转发:
ssh -L 8099:127.0.0.1:8099 你的主机实在要监听全网卡(或放到反代后面),先设 KEYBOX_WEBUI_PASSWORD 启用 Basic Auth。
没设密码时启动会警告一次。
其余几处的风险边界:
| 风险 | |
|---|---|
| 公钥挂到公网 | 无。那是公钥,发布它是非对称密码学的正常用法 |
DOMAIN |
只需提供一个静态文件,不需要指向容器、不需要开任何入站端口 |
| 代理的 443 | 默认不映射到宿主机。任何情况下都不要往公网转发 |
| 首页 | 不渲染 token 明文,要点一下才走 /api/refresh-token 单独取 |
| 私钥 | 只能签名不能授权,单独拿到无法下发命令;但和 token 同在一个卷里,备份务必加密 |
Tesla 的 refresh token 用一次就轮换。同一串被两个进程各自刷新时,后刷的会让 先刷的失效,表现是隔三差五掉线要求重新授权,而且极难排查。
代理本身是 OAuth 无状态的——它转发客户端带来的 Authorization 头,自己不需要
token。所以配置完成后,在向导页面点「从 keybox 删除」,让 Home Assistant
成为唯一持有者。
必填四项:CLIENT_ID CLIENT_SECRET DOMAIN REGION。
其余见 .env.example,每一项都真实生效。
界面语言与 REGION 无关:REGION 只决定连哪个 Tesla 端点,界面语言按浏览器
Accept-Language 判断,可用 ?lang= 或 FORCE_LANGUAGE 覆盖。
环境变量分三组,前缀本身就是语义:
| 前缀 | 含义 | 例子 |
|---|---|---|
| 无前缀 | Tesla 开发者后台里就叫这个名字的东西,以及网络/界面设置 | CLIENT_ID DOMAIN REGION PROXY_HOST |
TESLA_ |
Tesla API 那一侧的东西 | TESLA_ENDPOINT_TOKEN TESLA_SCOPES |
KEYBOX_ |
本程序自己的旋钮(路径等) | KEYBOX_STATE_DIR KEYBOX_PUBLIC_DIR |
UPSTREAM ← 唯一的版本钉子
├── Dockerfile 直接读它构建(不是 ARG 默认值,避免两处真相打架)
├── upstream-watch.yml 每天比对 Tesla 的 release,有新版自动开 PR
└── CI 断言容器里的二进制版本与它一致
升级流程:机器人开 PR → CI 构建镜像、启动、攻击它 → 绿了你点合并。 CI 红了就关掉 PR,留在旧版本上完全可以接受。
本仓库不是任何人的 fork,所以永远不会有需要手工解决的合并冲突。
| 工作流 | 触发 | 做什么 |
|---|---|---|
ci.yml |
push / PR | 单测、结构完整性、shellcheck、容器冒烟(路径门控见下) |
release.yml |
tag / main 且镜像内容有变 / 手动 | 构建三架构镜像推 GHCR |
upstream-watch.yml |
每天 cron | 比对 vehicle-command 新版,自动开 PR |
ghcr-cleanup.yml |
每周日 cron / 手动 | 清 GHCR 里的 untagged 孤儿版本 |
ghcr-cleanup 钉死 snok/container-retention-policy@v3.1.0,这是多架构安全的
最低版本:本项目推三个架构,GHCR 里天然存在无标签的平台子 manifest,
更早的版本按 untagged 清理会把 :latest 删坏。手动触发默认干跑。
smoke 要构建整个多架构镜像,是唯一昂贵的 job。所以每个工作流开头都有一个
门控 job,先算出这次改动碰了什么,再决定后面哪些 job 有意义。
| 改动 | unit | pins | tree | shell | smoke | 发布镜像 |
|---|---|---|---|---|---|---|
README / docs/ / NOTICE |
· | · | · | · | · | · |
docker-compose.yml / .env.example |
· | · | · | · | · | · |
.github/workflows/ci.yml |
● | ● | ● | ● | · | · |
.github/workflows/(其它) |
· | · | · | · | · | · |
tests/ |
● | · | · | · | · | · |
scripts/ |
· | · | ● | ● | · | · |
src/ / config/ |
● | · | ● | · | ● | ● |
rootfs/ |
· | · | ● | ● | ● | ● |
Dockerfile / UPSTREAM |
· | ● | ● | · | ● | ● |
推 tag v* |
— | — | — | — | — | ● |
改工作流本身会跑全部廉价 job(验证没写坏),但不重建镜像。
代价是:如果你改的正是 smoke 那一段,默认要等下次动代码才会暴露问题。
两个逃生舱:
- 提交信息里带
[smoke]或[full ci](push 和 PR 都生效) - Actions 页面 → Run workflow → 勾上 smoke
发布同理::latest 只在镜像内容真的变了才重新推。改 README 不会产生新镜像 ——
镜像没变,就不该有新镜像。
这个项目站在几个前人的肩膀上。下面把血缘关系说清楚,也说清楚哪些是直接用的、 哪些只是借鉴了思路。
teslamotors/vehicle-command — Tesla 官方
容器里运行的 tesla-http-proxy 和 tesla-keygen 两个二进制完全来自这里,
按 UPSTREAM 里钉住的 tag 从源码构建,本仓库不做任何修改。
这是唯一真正需要跟随的上游。
just-containers/s6-overlay (ISC) 容器内的进程监管,原样使用。
llamafilm/tesla-http-proxy-addon (Apache-2.0) 最早把整套流程封装成 HA 加载项的人。本项目的这些做法源自这里:
- 用 GnuPG +
pass做私钥保险箱,以及tesla-keygen导出公钥的调用方式 - 自签 EC 证书的 openssl 参数(secp521r1 +
extendedKeyUsage=serverAuth) - 把 Web UI 做成常驻的独立服务,而不是配置完就退出——这是本项目能砍掉 一整套进程状态机的关键,功劳全在这个设计决定上
- 启动时验证公钥公网可达再继续,而不是等用户报错才发现
该项目已于 2025 年 1 月停止维护(作者卖掉了 Tesla)。本仓库没有 fork 它, Python 部分是重写的,因此不存在需要持续合并的上游分支。
iainbullock/tesla-http-proxy-docker (Apache-2.0) 把上面那个加载项改造成独立 Docker 容器,让用 Docker 版 HA(没有加载项功能) 的人也能用。本项目沿用了"独立容器而非加载项"这个基本形态。
磁盘布局不兼容:那个项目用 /data + /share,本项目按 FHS 重排为
/etc + /var/lib + /srv,且密钥路径也变了。从那边迁移需要重新走一遍配置。
iainbullock 版把 token 交换硬编码到了 fleet-auth.prd.vn.cloud.tesla.com,
中国区用不了——这是本项目最初分出来的原因。
现在把端点拆成四条独立的轴,见 config/regions.json。
中国区 token 端点当前指向 auth.tesla.cn(实测跑通的配置)。Tesla 官方文档推荐
所有区域的 /token 统一走 fleet-auth.prd.vn.cloud.tesla.com,理由是服务端调用
走不同的限流池;遇到 unauthorized_client 或 429,设个环境变量即可切换,
不必改代码:
TESLA_ENDPOINT_TOKEN=https://fleet-auth.prd.vn.cloud.tesla.com顺带修了一个上游一直存在的问题:车辆配对深链在上游模板里被硬编码成
tesla.com/_ak/,中国用户点了拉不起 App。这里按区域走 tesla.cn。
alandtse/tesla (Apache-2.0)
Home Assistant 的 Tesla Custom Integration。它定义了本项目的输出契约:
Refresh Token、Proxy URL、SSL 证书路径、Client ID 这四个值。
scripts/keybox 的全部工作就是把这四个值算准。
Apache-2.0,与上述项目一致。见 LICENSE 与 NOTICE。
下一步是部署,不是继续写代码。 剩下的候选项大多需要生产环境才能判断该不该做:
tesla-keygen v0.4.1 的参数、China 区 token 端点、HA 的 domain 该填 .cn
还是 .com、token 多久失效一次——CI 一个都验证不了。
而唯一不可逆的风险(私钥丢失)./scripts/keybox backup 已经覆盖。
| 阶段 | 内容 |
|---|---|
| Phase 0 | 部署验证。把部署过程当成一次测试,逐步记录 |
| Phase 1 | 观察期 1–2 周,什么都不改,收集证据 |
| Phase 2+ | 按证据迭代——每一项都有明确的触发条件 |
完整清单、验证步骤、观察项见 docs/ROADMAP.md。
不加 /token 和 /command 端点。 那会让 keybox 成为第二个 token 持有者,
而 Tesla 的 refresh token 用一次就轮换,两个持有者会互相刷失效。
Home Assistant 已经把刷新、唤醒休眠车辆、429 退避都做好了。
keybox 的边界是「把一条签名过的命令可靠地送进车里」,不包括「决定发什么命令」 和「知道车现在什么状态」。后两者分别属于决策层和 TeslaMate。
先跑 ./scripts/keybox。详见 docs/TROUBLESHOOTING.md。
docs/ROADMAP.md—— 部署验证清单、观察期、按证据迭代的候选项docs/TROUBLESHOOTING.md—— 按故障出现顺序排列的排查链docs/CHARGING-AUTOMATION.md—— 谷电充电自动化的 跨仓库开发文档,含任务分解表(H-/K-/N-/C-)。说明了为什么 keybox 在这条 链路里保持薄
本项目与 Tesla, Inc. 无关,也未获其背书。请不要因为这个项目的问题去联系 Tesla 客服。