ROUND 1 ... FIGHT!

Tesla’s Vehicle Command SDK includes an HTTPS proxy that signs vehicle commands. I deployed the official Docker image on Unraid for use with evcc and a Tesla Wall Connector Gen 3.

The difficult part was not starting Docker: it was connecting the application registration, account permissions, and vehicle key in the correct order.

Verification status: The proxy is running, its HTTPS certificate validates, Tesla’s API returns my vehicle, and the virtual key has been enrolled. The evcc certificate mapping and final charging-command test are follow-up steps below, not verified outcomes of this setup session.

What is needed

  • Unraid with Docker and the Compose Manager plugin.
  • evcc running on the same Unraid server using host networking.
  • A Tesla developer application and access to the vehicle-owner account.
  • A public HTTPS domain for hosting the application’s public key.
  • The Tesla mobile app, signed in as the vehicle owner.

This setup uses Tesla’s Internet-based Fleet API, not Bluetooth. Self-hosting the proxy does not bypass Tesla’s API billing or quotas.

1. Create the Tesla developer application

Create an application in the Tesla developer dashboard. Save its Client ID and Client Secret privately.

For charging-only evcc use, enable:

PermissionOAuth scopePurpose
Vehicle Informationvehicle_device_dataIdentify the vehicle and read battery/charging data
Vehicle Charging Managementvehicle_charging_cmdsStart/stop charging and set charging current

The authorization request also includes openid and offline_access so the client can maintain access with refresh tokens. Broader Vehicle Commands and Vehicle Location permissions are not needed for this charging-only setup.

I used myTeslaMate’s OAuth callback to display the authorization code, while hosting my own command-signing key:

Developer settingValue used
Allowed Originhttps://dvkwong.com
Allowed Redirect URIhttps://app.myteslamate.com/auth/tesla/user/callback
Allowed Returned URLhttps://app.myteslamate.com/

The callback and key-hosting domain serve different purposes. The blog does not implement an OAuth callback. If using a different OAuth client, use its actual registered callback instead.

2. Store configuration on Unraid’s appdata share

Run the following commands in the Unraid terminal. The official image runs as UID/GID 65532.

Terminal window
mkdir -p /mnt/user/appdata/tesla-vehicle-command/config
chown 65532:65532 /mnt/user/appdata/tesla-vehicle-command/config
chmod 700 /mnt/user/appdata/tesla-vehicle-command/config

Do not keep the keys in a relative ./config directory under the Compose Manager project on /boot. On this server, /boot is FAT with root-only permissions. The container failed with:

Error: open /config/fleet-key.pem: permission denied

An absolute appdata bind mount avoids that filesystem-permission problem.

3. Generate separate signing and HTTPS keys

The signing key authenticates commands to the vehicle. The TLS key secures the proxy connection. They must be different keys.

Run once, before any key has been enrolled. Do not overwrite existing keys when repeating the setup.

Terminal window
(
set -eu
umask 077
cd /mnt/user/appdata/tesla-vehicle-command/config
for file in fleet-key.pem public-key.pem tls-key.pem tls-cert.pem; do
if [ -e "$file" ]; then
echo "STOP: $file already exists; preserve the existing keys."
exit 1
fi
done
openssl ecparam -name prime256v1 -genkey -noout -out fleet-key.pem
openssl pkey -in fleet-key.pem -pubout -out public-key.pem
openssl req -x509 -nodes -newkey ec \
-pkeyopt ec_paramgen_curve:secp384r1 \
-subj '/CN=unraid' \
-keyout tls-key.pem -out tls-cert.pem -sha256 -days 825 \
-addext 'subjectAltName=DNS:unraid,DNS:localhost,IP:127.0.0.1' \
-addext 'extendedKeyUsage=serverAuth' \
-addext 'keyUsage=digitalSignature'
chown 65532:65532 fleet-key.pem public-key.pem tls-key.pem tls-cert.pem
chmod 600 fleet-key.pem tls-key.pem
chmod 644 public-key.pem tls-cert.pem
)

Back up the keys securely. Replacing the signing key requires enrolling the new public key in the vehicle.

4. Configure and start the Docker proxy

In Compose Manager, create a Tesla Vehicle Command project using:

services:
tesla_http_proxy:
image: tesla/vehicle-command:latest
restart: unless-stopped
ports:
- "127.0.0.1:4443:4443"
environment:
TESLA_HTTP_PROXY_TLS_CERT: /config/tls-cert.pem
TESLA_HTTP_PROXY_TLS_KEY: /config/tls-key.pem
TESLA_HTTP_PROXY_HOST: 0.0.0.0
TESLA_HTTP_PROXY_PORT: "4443"
TESLA_HTTP_PROXY_TIMEOUT: 10s
TESLA_KEY_FILE: /config/fleet-key.pem
TESLA_VERBOSE: "false"
security_opt:
- no-new-privileges:true
volumes:
- /mnt/user/appdata/tesla-vehicle-command/config:/config:ro

Use Compose Up, then test from Unraid:

Terminal window
curl --cacert /mnt/user/appdata/tesla-vehicle-command/config/tls-cert.pem \
https://127.0.0.1:4443/api/1/vehicles

Without an OAuth token, the expected result is HTTP 403 with client did not provide an OAuth token. This verifies the HTTPS listener, not vehicle access.

Only loopback is published. Do not expose port 4443 through a router or Cloudflare Tunnel. evcc can reach it because it uses host networking on the same server. A bridge-networked or remote evcc instance needs a different, secured network arrangement.

5. Publish only the public key

Display or copy this file:

Terminal window
cat /mnt/user/appdata/tesla-vehicle-command/config/public-key.pem

Host its raw PEM contents at this exact path on your domain:

https://YOUR_DOMAIN/.well-known/appspecific/com.tesla.3p.public-key.pem

For my Astro blog, the source file is public/.well-known/appspecific/com.tesla.3p.public-key.pem, published at:

https://dvkwong.com/.well-known/appspecific/com.tesla.3p.public-key.pem

Cloudflare or any HTTPS static host can serve it. The response must be the public key, not HTML, a login page, or a bot challenge. Keep the URL available after enrollment.

Never publish fleet-key.pem, tls-key.pem, OAuth tokens, or the client secret.

6. Store OAuth application credentials privately

These credentials are used for token requests, not placed in the proxy’s environment.

Terminal window
umask 077
mkdir -p /mnt/user/appdata/tesla-vehicle-command/oauth
chmod 700 /mnt/user/appdata/tesla-vehicle-command/oauth
nano /mnt/user/appdata/tesla-vehicle-command/oauth/client.json

Enter your own values:

{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}

This directory is outside the proxy’s mounted configuration. Do not commit it to Git or share its contents.

7. Register the public-key domain with Tesla

Publishing the key and adding Allowed Origins do not perform Fleet API partner registration.

The following requests a temporary partner token and registers the domain directly with Tesla. It uses the NA Fleet API endpoint, which was confirmed as the correct region for my New Zealand account. Other accounts should use the appropriate regional endpoint.

Change domain before running this on another installation:

Terminal window
(
set -eu
umask 077
cd /mnt/user/appdata/tesla-vehicle-command/oauth
domain=dvkwong.com
api=https://fleet-api.prd.na.vn.cloud.tesla.com
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
jq -rj --arg audience "$api" \
'{grant_type:"client_credentials",client_id,client_secret,
scope:"openid vehicle_device_data vehicle_charging_cmds",
audience:$audience}
| to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&")' \
client.json > "$work/request"
curl --fail-with-body --silent --show-error --max-time 45 \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-binary @"$work/request" \
https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token \
-o "$work/token"
jq -er '"Authorization: Bearer " + (.access_token | select(length > 0))' \
"$work/token" > "$work/header"
jq -n --arg domain "$domain" '{domain:$domain}' > "$work/registration"
curl --fail-with-body --silent --show-error --max-time 45 \
-H @"$work/header" -H 'Content-Type: application/json' \
--data-binary @"$work/registration" \
"$api/api/1/partner_accounts" -o "$work/result"
jq '{response,error,error_description}' "$work/result"
)

Successful registration returned HTTP 200 with domain: dvkwong.com. The published public key was also checked against the proxy’s private key.

Use jq -rj, not just jq -r, when creating the form body: a trailing newline in the final audience value caused an invalid_audience error during setup.

Do not rerun myTeslaMate’s full registration script for this self-hosted domain. Its registration step targets a myTeslaMate hostname, which produced a root-domain/Allowed Origin mismatch in this setup.

8. Authorize the vehicle-owner account

A partner token registers the application; it is not a user access token. The vehicle owner must separately grant access.

Generate a login URL on Unraid:

Terminal window
(
set -eu
umask 077
cd /mnt/user/appdata/tesla-vehicle-command/oauth
state=$(openssl rand -hex 32)
printf '%s' "$state" > authorization-state
jq -r --arg state "$state" \
'"https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/authorize?" +
({client_id,redirect_uri:"https://app.myteslamate.com/auth/tesla/user/callback",
response_type:"code",
scope:"openid offline_access vehicle_device_data vehicle_charging_cmds",
state:$state,prompt:"consent"}
| to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&"))' \
client.json
)

Open that URL, sign in with the same owner account used in the Tesla app, select the vehicle, and grant Vehicle Information and Vehicle Charging Management. For a self-hosted callback, validate the returned state against the stored value; this walkthrough uses myTeslaMate’s callback/code-display flow.

If no consent screen appears and the required permissions remain missing, revoke only this application under the Tesla account’s Security → Third-Party Apps, then start a fresh authorization flow. Revocation invalidates existing tokens for that application.

Exchange the authorization code immediately

Codes expire quickly and work only once. Run this on Unraid and paste only the newly issued code at the hidden prompt:

Terminal window
(
set -eu
umask 077
cd /mnt/user/appdata/tesla-vehicle-command/oauth
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
read -r -s -p 'New authorization code: ' code
printf '\n'
printf '%s' "$code" > "$work/code"
unset code
jq -rj --rawfile code "$work/code" \
'{grant_type:"authorization_code",client_id,client_secret,
code:$code,redirect_uri:"https://app.myteslamate.com/auth/tesla/user/callback"}
| to_entries | map((.key|@uri)+"="+(.value|@uri)) | join("&")' \
client.json > "$work/request"
curl --silent --show-error --max-time 45 \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-binary @"$work/request" \
https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token \
-o "$work/response"
if jq -e '(.access_token|type=="string" and length>0)
and (.refresh_token|type=="string" and length>0)' \
"$work/response" >/dev/null; then
install -m 600 "$work/response" tokens.json
rm -f authorization-state
echo 'SUCCESS: OAuth tokens saved privately.'
else
jq '{error,error_description}' "$work/response"
exit 1
fi
)

A successful exchange does not prove that all requested permissions were granted. Initially, my token contained charging permission only; listing vehicles returned 403 Unauthorized missing scopes. Reauthorizing with Vehicle Information fixed vehicle access.

9. Enroll the virtual key in the Tesla app

Do this after account consent, not before.

  1. Wake the vehicle and select it in the Tesla app.
  2. On the phone, open https://tesla.com/_ak/dvkwong.com (replace the domain for another installation).
  3. Approve the virtual-key request.
  4. Check Controls → Locks in the vehicle for the new key.

Before account consent was complete, the app reported:

Adding a virtual key for "dvkwong.com" is forbidden because you have not
granted "dvkwong.com" access to your account.

The key was successfully enrolled after correcting the OAuth consent flow. No new signing key was needed.

10. Let evcc trust the proxy certificate

In Unraid, choose evcc → Edit → Add another Path, Port, Variable, Label or Device, then add a Path:

SettingValue
Host path/mnt/user/appdata/tesla-vehicle-command/config/tls-cert.pem
Container path/etc/ssl/certs/tesla-proxy.pem
Access modeRead Only

Apply the change and restart evcc. This mount is intended to make the self-signed certificate available in the Alpine-based container’s certificate directory; verify actual trust with a charging request before considering integration complete. Do not disable TLS verification or mount either private key into evcc.

Persist the mapping in the Unraid template, not with a one-off docker cp, so container recreation retains it.

11. Configure the vehicle and loadpoint in evcc

Open Configuration → Vehicles → Add → Tesla, not Tesla BLE.

FieldValue
Client IDYour Tesla application’s Client ID
Access tokenLatest user OAuth access token
Refresh tokenMatching user OAuth refresh token
VINYour vehicle’s VIN
Command Proxy, under advanced settingshttps://127.0.0.1:4443
Proxy TokenLeave empty; this is for the paid myTeslaMate proxy

To display the saved tokens only in your own Unraid terminal:

Terminal window
jq '{access_token, refresh_token}' \
/mnt/user/appdata/tesla-vehicle-command/oauth/tokens.json

Copy them into evcc; do not share screenshots or paste them into chat. evcc should manage subsequent token refreshes. The manually saved file is a bootstrap snapshot, not an automatically synchronized copy of evcc’s current tokens.

Then open Configuration → Loadpoints → Tesla Charger → Edit, select the newly added vehicle, and save. Keep charging mode Off until testing is ready.

12. Verify before enabling automation

  • Confirm the container stays running without a restart loop.
  • Confirm HTTPS works with certificate verification enabled.
  • Confirm evcc can read the correct vehicle’s battery level.
  • With the car plugged in and ready, deliberately test charging start/stop and current adjustment.
  • Check evcc and proxy logs for TLS, OAuth, or key-pairing errors.

Reading battery data verifies OAuth access, but does not verify signed commands. Only a successful charging command completes the end-to-end check. At the time of writing, virtual-key enrollment and Fleet API vehicle access had succeeded; this final evcc charging test was still outstanding.

References