Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

tesla-keybox

给 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

然后:

  1. 把 ./public 挂给你的反向代理当 webroot,确认这个 URL 返回 200: https://<DOMAIN>/.well-known/appspecific/com.tesla.3p.public-key.pem
  2. 打开 http://<服务器>:8099,按页面上的五步走完
  3. ./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 同在一个卷里,备份务必加密

填进 Home Assistant 之后,把这里的删掉

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 删坏。手动触发默认干跑。


CI 的路径门控

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 客服。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages