commaai/openpilot · error · RuntimeError

private key is not configured

Error message

private key is not configured

What it means

Api.get_token() signs a JWT with the device's private key to authenticate to the comma API. The Api constructor loads the key pair via get_key_pair(), which scans PERSIST/comma/id_rsa(.pub) and id_ecdsa(.pub). If neither pair exists on disk, private_key stays None and get_token raises this RuntimeError instead of attempting to sign.

Source

Thrown at openpilot/common/api.py:31


class Api:
  def __init__(self, dongle_id):
    self.dongle_id = dongle_id
    self.jwt_algorithm, self.private_key, _ = get_key_pair()

  def get(self, *args, **kwargs):
    return self.request('GET', *args, **kwargs)

  def post(self, *args, **kwargs):
    return self.request('POST', *args, **kwargs)

  def request(self, method, endpoint, timeout=None, access_token=None, **params):
    return api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)

  def get_token(self, payload_extra=None, expiry_hours=1):
    if self.private_key is None:
      raise RuntimeError("private key is not configured")
    now = datetime.now(UTC).replace(tzinfo=None)
    payload = {
      'identity': self.dongle_id,
      'nbf': now,
      'iat': now,
      'exp': now + timedelta(hours=expiry_hours)
    }
    if payload_extra is not None:
      payload.update(payload_extra)
    token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
    if isinstance(token, bytes):
      token = token.decode('utf8')
    return token


def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params):
  headers = {}
  if access_token is not None:

View on GitHub (pinned to 516ec1e682)

Solutions

  1. Generate an RSA or ECDSA key pair in the persist directory: mkdir -p <persist>/comma && ssh-keygen -t ecdsa -f <persist>/comma/id_ecdsa (no passphrase), so both id_ecdsa and id_ecdsa.pub exist
  2. If keys exist but under a different Paths.persist_root(), verify Paths.persist_root() resolves where you expect and move/copy the comma/ directory there
  3. Guard calls: only invoke get_token() after checking api.get_key_pair() returns a non-None key, and skip/queue API-authenticated work otherwise
  4. On a real device, let the provisioning flow (e.g. setup/registration) create the keys before any Api usage

Example fix

// before
api = Api(dongle_id)
token = api.get_token()  # RuntimeError: private key is not configured

# after
from openpilot.common.api import Api, get_key_pair

algorithm, private_key, _ = get_key_pair()
if private_key is None:
  print('device not provisioned with keys; skipping API token')
else:
  token = Api(dongle_id).get_token()
Defensive patterns

Strategy: validation

Validate before calling

from openpilot.common.api import get_key_pair

algorithm, private_key, public_key = get_key_pair()
if private_key is None:
  # device not provisioned; skip or trigger provisioning
  raise SystemExit('no comma key pair in persist; provision the device first')

Type guard

def has_private_key() -> bool:
  from openpilot.common.api import get_key_pair
  return get_key_pair()[1] is not None

Try / catch

try:
  token = api.get_token()
except RuntimeError as e:
  if 'private key is not configured' in str(e):
    # provision keys or degrade gracefully
    ...
  raise

Prevention

When it happens

Trigger: Calling Api(dongle_id).get_token(...) on a device (or dev machine) where Paths.persist_root()/comma/ does not contain both id_rsa and id_rsa.pub, or both id_ecdsa and id_ecdsa.pub. This happens on fresh installations before key generation, on non-device PCs without a persisted /comma directory, or if the persist partition was wiped.

Common situations: Running openpilot code off-device (CI, laptops) without copying the persist partition; a factory reset or re-flash that cleared /persist/comma; a partially provisioned device where only the .pub file exists (get_key_pair requires both files).

Related errors


AI-assisted analysis of commaai/openpilot@516ec1e682 (2026-08-15). Data as JSON: /api/errors/654298a0c49e90cb. Report an issue: GitHub.