Files
OPNexus/backend/app/services/wireguard_key_store.py
T
Julian Boldt e51df4ba04
Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release v1.11.0-rc1
Siehe CHANGELOG.md
2026-10-07 12:24:38 +02:00

83 lines
3.1 KiB
Python

"""Per-tunnel WireGuard secret storage: the Server's private key (always
required — see wireguard_deploy_service.py's module docstring for why
OPNsense's model doesn't derive one from a supplied public key or vice
versa) and the Peer's optional pre-shared key (WireGuard's own per-peer
`psk`, unrelated to IPsec's PSK despite the shared name).
Mirrors ipsec_psk_store.py/openvpn_key_store.py's convention exactly: raw
secret material never touches the database as its own column, only disk,
one file per tunnel id per secret kind, mode 0600.
WireguardTunnel.server_private_key_set/peer_psk_set are the DB-side flags
tracking whether each file exists.
Unlike openvpn_key_store.py (one secret kind -> one dedicated module),
this file handles BOTH of this entity's secrets in one place — both are
addressed by the same tunnel id, and the entity has exactly two secret
kinds by construction (never more), so two thin near-duplicate modules
would add nothing except upkeep. Documented as a deliberate deviation
from the one-file-per-secret-kind precedent, not an oversight.
Note (documented, not a new issue introduced here): exactly like
ipsec_deploy_service.py's/openvpn_deploy_service.py's build_payloads(),
both secrets end up embedded in the payloads dict persisted as
WireguardTunnelDeployment.last_payload (a Postgres JSON column), for the
same no-op-redeploy-detection reason — matched here for consistency with
the established pattern rather than diverging from it.
"""
import uuid
from pathlib import Path
from app.config import settings
from app.services import secure_files
_PRIVATE_KEY_SUFFIX = ".wgkey"
_PSK_SUFFIX = ".wgpsk"
def _path(tunnel_id: uuid.UUID, suffix: str) -> Path:
return Path(settings.wireguard_key_secret_dir) / f"{tunnel_id}{suffix}"
def save_private_key(tunnel_id: uuid.UUID, secret: str) -> None:
path = _path(tunnel_id, _PRIVATE_KEY_SUFFIX)
secure_files.write_private(path, secret.strip())
def read_private_key(tunnel_id: uuid.UUID) -> str | None:
path = _path(tunnel_id, _PRIVATE_KEY_SUFFIX)
if not path.exists():
return None
return path.read_text().strip()
def delete_private_key(tunnel_id: uuid.UUID) -> None:
_path(tunnel_id, _PRIVATE_KEY_SUFFIX).unlink(missing_ok=True)
def save_psk(tunnel_id: uuid.UUID, secret: str) -> None:
path = _path(tunnel_id, _PSK_SUFFIX)
secure_files.write_private(path, secret.strip())
def read_psk(tunnel_id: uuid.UUID) -> str | None:
path = _path(tunnel_id, _PSK_SUFFIX)
if not path.exists():
return None
return path.read_text().strip()
def delete_psk(tunnel_id: uuid.UUID) -> None:
_path(tunnel_id, _PSK_SUFFIX).unlink(missing_ok=True)
def version(tunnel_id: uuid.UUID) -> str:
"""Opaque change marker for both secrets (last write times) — lets an approval
token notice a rotated key/PSK without containing, or being derivable from, them."""
marks = []
for suffix in (_PRIVATE_KEY_SUFFIX, _PSK_SUFFIX):
try:
marks.append(str(_path(tunnel_id, suffix).stat().st_mtime_ns))
except FileNotFoundError:
marks.append("-")
return ":".join(marks)