=== STEP_034B VM121 PEER TOOLING INSPECT === timestamp=20260708-113518 mode=read-only goal: identify existing WG Access command/API/smoke path for creating/removing a dedicated test WGPay peer secrets_policy: redact tokens, private keys, passwords, DB URLs, client private keys === host === wg-access-dev Wed Jul 8 11:35:19 UTC 2026 === docker containers === NAMES IMAGE STATUS PORTS wgaccess-backend python:3.12-slim Up 41 hours 0.0.0.0:18080->8000/tcp wgaccess-postgres postgres:16-alpine Up 41 hours (healthy) 127.0.0.1:15432->5432/tcp === backend app tree python/router files === /opt/wg-access/backend/alembic/env.py /opt/wg-access/backend/alembic/versions/0001_initial_schema.py /opt/wg-access/backend/app/api/admin.py /opt/wg-access/backend/app/api/agent.py /opt/wg-access/backend/app/api/dev.py /opt/wg-access/backend/app/config.py /opt/wg-access/backend/app/db/base.py /opt/wg-access/backend/app/db/session.py /opt/wg-access/backend/app/main.py /opt/wg-access/backend/app/models/__init__.py /opt/wg-access/backend/app/models/core.py /opt/wg-access/backend/app/services/__init__.py /opt/wg-access/backend/app/services/wireguard.py === backend routes/endpoints from source grep === /opt/wg-access/backend/app/main.py:17:@app.get("/health") /opt/wg-access/backend/app/main.py:26:@app.get("/health/db") /opt/wg-access/backend/app/api/admin.py:6:from fastapi import APIRouter, Depends, HTTPException /opt/wg-access/backend/app/api/admin.py:13:from app.services.wireguard import ( /opt/wg-access/backend/app/api/admin.py:20:router = APIRouter(prefix="/admin", tags=["admin"]) /opt/wg-access/backend/app/api/admin.py:41:@router.post("/invites", response_model=InviteResponse) /opt/wg-access/backend/app/api/admin.py:57:@router.get("/invites", response_model=list[InviteResponse]) /opt/wg-access/backend/app/api/admin.py:77:@router.get("/peers", response_model=list[PeerResponse]) /opt/wg-access/backend/app/api/admin.py:106:@router.post("/subscriptions", response_model=SubscriptionCreateResponse) /opt/wg-access/backend/app/api/admin.py:109: # It creates a user, an active subscription, a WG peer and a pending enable_peer job. /opt/wg-access/backend/app/api/admin.py:146: action="enable_peer", /opt/wg-access/backend/app/api/admin.py:188:@router.post("/maintenance/expire-subscriptions", response_model=ExpireSubscriptionsResponse) /opt/wg-access/backend/app/api/admin.py:220: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:254:@router.post("/subscriptions/{subscription_id}/cancel", response_model=CancelSubscriptionResponse) /opt/wg-access/backend/app/api/admin.py:283: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:319:@router.post("/peers/{peer_id}/disable", response_model=DisablePeerResponse) /opt/wg-access/backend/app/api/admin.py:320:def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): /opt/wg-access/backend/app/api/admin.py:331: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:369:@router.get("/jobs", response_model=list[JobResponse]) /opt/wg-access/backend/app/api/admin.py:439:@router.get("/users", response_model=list[AdminUserSummaryResponse]) /opt/wg-access/backend/app/api/admin.py:498:@router.get("/users/by-email/{email}", response_model=AdminUserDetailResponse) /opt/wg-access/backend/app/api/admin.py:514:@router.get("/users/{user_id}", response_model=AdminUserDetailResponse) /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:8:from fastapi import APIRouter, Depends /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:16:router = APIRouter(prefix="/dev", tags=["dev"]) /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:71:@router.post("/create-test-peer", response_model=TestPeerResponse) /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:97: action="enable_peer", /opt/wg-access/backend/app/api/dev.py:4:from fastapi import APIRouter, Depends, HTTPException /opt/wg-access/backend/app/api/dev.py:11:from app.services.wireguard import ( /opt/wg-access/backend/app/api/dev.py:18:router = APIRouter(prefix="/dev", tags=["dev"]) /opt/wg-access/backend/app/api/dev.py:44:@router.post("/create-test-peer", response_model=TestPeerResponse) /opt/wg-access/backend/app/api/dev.py:78: action="enable_peer", /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:5:from fastapi import APIRouter, Depends /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:13:router = APIRouter(prefix="/dev", tags=["dev"]) /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:44:@router.post("/create-test-peer", response_model=TestPeerResponse) /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:67: action="enable_peer", /opt/wg-access/backend/app/api/agent.py:4:from fastapi import APIRouter, Depends, Header, HTTPException /opt/wg-access/backend/app/api/agent.py:13:router = APIRouter(prefix="/agent", tags=["agent"]) /opt/wg-access/backend/app/api/agent.py:34:@router.get("/peers", response_model=list[AgentPeerResponse]) /opt/wg-access/backend/app/api/agent.py:68:@router.get("/jobs", response_model=list[AgentJobResponse]) /opt/wg-access/backend/app/api/agent.py:85:@router.post("/jobs/{job_id}/start", response_model=AgentJobResponse) /opt/wg-access/backend/app/api/agent.py:106:@router.post("/jobs/{job_id}/complete", response_model=AgentJobResponse) /opt/wg-access/backend/app/api/agent.py:127:@router.post("/jobs/{job_id}/fail", response_model=AgentJobResponse) /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:5:from fastapi import APIRouter, Depends /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:13:router = APIRouter(prefix="/admin", tags=["admin"]) /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:34:@router.post("/invites", response_model=InviteResponse) /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:50:@router.get("/invites", response_model=list[InviteResponse]) /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:70:@router.get("/peers", response_model=list[PeerResponse]) /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:89:@router.get("/jobs", response_model=list[JobResponse]) /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:4:from fastapi import APIRouter, Depends, Header, HTTPException /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:13:router = APIRouter(prefix="/agent", tags=["agent"]) /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:39:@router.get("/jobs", response_model=list[AgentJobResponse]) /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:56:@router.post("/jobs/{job_id}/start", response_model=AgentJobResponse) /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:77:@router.post("/jobs/{job_id}/complete", response_model=AgentJobResponse) /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:98:@router.post("/jobs/{job_id}/fail", response_model=AgentJobResponse) === backend models/schemas relevant grep === /opt/wg-access/backend/app/models/core.py:52: status: Mapped[str] = mapped_column(String(32), default="created", nullable=False, index=True) /opt/wg-access/backend/app/models/core.py:62:class Subscription(Base): /opt/wg-access/backend/app/models/core.py:68: status: Mapped[str] = mapped_column(String(32), default="active", nullable=False, index=True) /opt/wg-access/backend/app/models/core.py:70: auto_renew: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) /opt/wg-access/backend/app/models/core.py:72: paid_until: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) /opt/wg-access/backend/app/models/core.py:81:class Peer(Base): /opt/wg-access/backend/app/models/core.py:92: public_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) /opt/wg-access/backend/app/models/core.py:96: paid_until: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) /opt/wg-access/backend/app/models/core.py:115: status: Mapped[str] = mapped_column(String(32), default="pending", nullable=False, index=True) /opt/wg-access/backend/app/main.py:20: "status": "ok", /opt/wg-access/backend/app/main.py:31: "status": "ok", /opt/wg-access/backend/app/services/wireguard.py:26: private_key = /opt/wg-access/backend/app/services/wireguard.py:27: public_key = private_key.public_key() /opt/wg-access/backend/app/services/wireguard.py:29: public_raw = public_key.public_bytes( /opt/wg-access/backend/app/services/wireguard.py:43: # - pool is /16 by default: 10.253.0.0/16 /opt/wg-access/backend/app/services/wireguard.py:44: # - client addresses start at 10.253.1.10 /opt/wg-access/backend/app/services/wireguard.py:47: pool_cidr = os.environ.get("WG_CLIENT_POOL_CIDR", "10.253.0.0/16").strip() /opt/wg-access/backend/app/services/wireguard.py:48: first_client_ip = os.environ.get("WG_CLIENT_FIRST_IP", "10.253.1.10").strip() /opt/wg-access/backend/app/services/wireguard.py:77:def build_client_config(private_key: , tunnel_ip: str, preshared_key: str) -> str: /opt/wg-access/backend/app/services/wireguard.py:78: server_public_key = os.environ.get("WG_SERVER_PUBLIC_KEY", "").strip() /opt/wg-access/backend/app/services/wireguard.py:80: allowed_ips = os.environ.get("WG_CLIENT_ALLOWED_IPS", "10.253.1.1/32").strip() /opt/wg-access/backend/app/services/wireguard.py:83: if not server_public_key: /opt/wg-access/backend/app/services/wireguard.py:90: f"PrivateKey = ", /opt/wg-access/backend/app/services/wireguard.py:100: f"PublicKey = {server_public_key}", /opt/wg-access/backend/app/services/wireguard.py:103: f"AllowedIPs = {allowed_ips}", /opt/wg-access/backend/app/api/admin.py:17: build_client_config, /opt/wg-access/backend/app/api/admin.py:63:class PeerResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:67: public_key: str /opt/wg-access/backend/app/api/admin.py:69: paid_until: datetime /opt/wg-access/backend/app/api/admin.py:84:class SubscriptionCreateRequest(BaseModel): /opt/wg-access/backend/app/api/admin.py:89: auto_renew: bool = False /opt/wg-access/backend/app/api/admin.py:92:class SubscriptionCreateResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:99: paid_until: datetime /opt/wg-access/backend/app/api/admin.py:100: private_key: /opt/wg-access/backend/app/api/admin.py:101: public_key: str /opt/wg-access/backend/app/api/admin.py:103: client_config: str /opt/wg-access/backend/app/api/admin.py:109: # It creates a user, an active subscription, a WG peer and a pending enable_peer job. /opt/wg-access/backend/app/api/admin.py:111: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/admin.py:119: status="active", /opt/wg-access/backend/app/api/admin.py:121: auto_renew=payload.auto_renew, /opt/wg-access/backend/app/api/admin.py:122: paid_until=paid_until, /opt/wg-access/backend/app/api/admin.py:128: private_key, public_key = generate_wg_keypair() /opt/wg-access/backend/app/api/admin.py:130: client_config = build_client_config(private_key, tunnel_ip, preshared_key) /opt/wg-access/backend/app/api/admin.py:135: public_key=public_key, /opt/wg-access/backend/app/api/admin.py:138: paid_until=paid_until, /opt/wg-access/backend/app/api/admin.py:146: action="enable_peer", /opt/wg-access/backend/app/api/admin.py:149: "public_key": peer.public_key, /opt/wg-access/backend/app/api/admin.py:152: "paid_until": peer.paid_until.isoformat(), /opt/wg-access/backend/app/api/admin.py:154: status="pending", /opt/wg-access/backend/app/api/admin.py:171: paid_until=peer.paid_until, /opt/wg-access/backend/app/api/admin.py:172: private_key=, /opt/wg-access/backend/app/api/admin.py:173: public_key=peer.public_key, /opt/wg-access/backend/app/api/admin.py:175: client_config=client_config, /opt/wg-access/backend/app/api/admin.py:181:class ExpireSubscriptionsResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:194: .where(Subscription.status == "active") /opt/wg-access/backend/app/api/admin.py:195: .where(Subscription.paid_until <= now) /opt/wg-access/backend/app/api/admin.py:196: .order_by(Subscription.paid_until.asc()) /opt/wg-access/backend/app/api/admin.py:203: subscription.status = "expired" /opt/wg-access/backend/app/api/admin.py:204: subscription.auto_renew = False /opt/wg-access/backend/app/api/admin.py:220: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:223: "public_key": peer.public_key, /opt/wg-access/backend/app/api/admin.py:226: status="pending", /opt/wg-access/backend/app/api/admin.py:245:class CancelSubscriptionResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:248: status: str /opt/wg-access/backend/app/api/admin.py:258: raise HTTPException(status_code=, detail="subscription not found") /opt/wg-access/backend/app/api/admin.py:264: subscription.status = "canceled" /opt/wg-access/backend/app/api/admin.py:265: subscription.auto_renew = False /opt/wg-access/backend/app/api/admin.py:283: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:286: "public_key": peer.public_key, /opt/wg-access/backend/app/api/admin.py:289: status="pending", /opt/wg-access/backend/app/api/admin.py:303: status=subscription.status, /opt/wg-access/backend/app/api/admin.py:310:class DisablePeerResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:314: public_key: str /opt/wg-access/backend/app/api/admin.py:320:def disable_peer(peer_id: UUID, db: Session = Depends(get_db)): /opt/wg-access/backend/app/api/admin.py:324: raise HTTPException(status_code=, detail="peer not found") /opt/wg-access/backend/app/api/admin.py:331: action="disable_peer", /opt/wg-access/backend/app/api/admin.py:334: "public_key": peer.public_key, /opt/wg-access/backend/app/api/admin.py:337: status="pending", /opt/wg-access/backend/app/api/admin.py:349: public_key=peer.public_key, /opt/wg-access/backend/app/api/admin.py:360: status: str /opt/wg-access/backend/app/api/admin.py:375:class AdminSubscriptionResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:378: status: str /opt/wg-access/backend/app/api/admin.py:380: auto_renew: bool /opt/wg-access/backend/app/api/admin.py:381: paid_until: datetime /opt/wg-access/backend/app/api/admin.py:389:class AdminUserPeerResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py:393: public_key: str /opt/wg-access/backend/app/api/admin.py:395: paid_until: datetime /opt/wg-access/backend/app/api/admin.py:409: status: str /opt/wg-access/backend/app/api/admin.py:509: raise HTTPException(status_code=, detail="user not found") /opt/wg-access/backend/app/api/admin.py:518: raise HTTPException(status_code=, detail="user not found") /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:19:class TestPeerRequest(BaseModel): /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:25:class TestPeerResponse(BaseModel): /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:30: paid_until: datetime /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:31: private_key: /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:32: public_key: str /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:48: private_key = /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:49: public_key = private_key.public_key() /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:51: public_raw = public_key.public_bytes( /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:64: # MVP-простота: 10.253.1.N, начиная с 10. /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:68: return f"10.253.1.{10 + count}" /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:77: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:80: private_key, public_key = generate_wg_keypair() /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:86: public_key=public_key, /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:89: paid_until=paid_until, /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:97: action="enable_peer", /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:100: "public_key": peer.public_key, /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:103: "paid_until": peer.paid_until.isoformat(), /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:105: status="pending", /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:119: paid_until=peer.paid_until, /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:120: private_key=, /opt/wg-access/backend/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636:121: public_key=peer.public_key, /opt/wg-access/backend/app/api/dev.py:15: build_client_config, /opt/wg-access/backend/app/api/dev.py:22: raise HTTPException(status_code=, detail="not found") /opt/wg-access/backend/app/api/dev.py:26:class TestPeerRequest(BaseModel): /opt/wg-access/backend/app/api/dev.py:32:class TestPeerResponse(BaseModel): /opt/wg-access/backend/app/api/dev.py:37: paid_until: datetime /opt/wg-access/backend/app/api/dev.py:38: private_key: /opt/wg-access/backend/app/api/dev.py:39: public_key: str /opt/wg-access/backend/app/api/dev.py:41: client_config: str /opt/wg-access/backend/app/api/dev.py:57: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/dev.py:60: private_key, public_key = generate_wg_keypair() /opt/wg-access/backend/app/api/dev.py:62: client_config = build_client_config(private_key, tunnel_ip, preshared_key) /opt/wg-access/backend/app/api/dev.py:67: public_key=public_key, /opt/wg-access/backend/app/api/dev.py:70: paid_until=paid_until, /opt/wg-access/backend/app/api/dev.py:78: action="enable_peer", /opt/wg-access/backend/app/api/dev.py:81: "public_key": peer.public_key, /opt/wg-access/backend/app/api/dev.py:84: "paid_until": peer.paid_until.isoformat(), /opt/wg-access/backend/app/api/dev.py:86: status="pending", /opt/wg-access/backend/app/api/dev.py:100: paid_until=peer.paid_until, /opt/wg-access/backend/app/api/dev.py:101: private_key=, /opt/wg-access/backend/app/api/dev.py:102: public_key=peer.public_key, /opt/wg-access/backend/app/api/dev.py:104: client_config=client_config, /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:16:class TestPeerRequest(BaseModel): /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:22:class TestPeerResponse(BaseModel): /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:27: paid_until: datetime /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:28: public_key: str /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:37: # MVP-простота: 10.253.1.N, начиная с 10. /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:41: return f"10.253.1.{10 + count}" /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:50: paid_until = datetime.now(timezone.utc) + timedelta(days=30 * payload.months) /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:56: public_key=fake_wg_key("pub"), /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:59: paid_until=paid_until, /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:67: action="enable_peer", /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:70: "public_key": peer.public_key, /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:73: "paid_until": peer.paid_until.isoformat(), /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:75: status="pending", /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:89: paid_until=peer.paid_until, /opt/wg-access/backend/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545:90: public_key=peer.public_key, /opt/wg-access/backend/app/api/agent.py:18: raise HTTPException(status_code=, detail="invalid agent token" /opt/wg-access/backend/app/api/agent.py:21:class AgentPeerResponse(BaseModel): /opt/wg-access/backend/app/api/agent.py:24: public_key: str /opt/wg-access/backend/app/api/agent.py:27: paid_until: datetime /opt/wg-access/backend/app/api/agent.py:56: status: str /opt/wg-access/backend/app/api/agent.py:78: .where(ProvisioningJob.status == "pending") /opt/wg-access/backend/app/api/agent.py:93: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py:94: if job.status != "pending": /opt/wg-access/backend/app/api/agent.py:95: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py:97: job.status = "running" /opt/wg-access/backend/app/api/agent.py:114: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py:115: if job.status not in ("pending", "running"): /opt/wg-access/backend/app/api/agent.py:116: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py:118: job.status = "completed" /opt/wg-access/backend/app/api/agent.py:136: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py:137: if job.status not in ("pending", "running"): /opt/wg-access/backend/app/api/agent.py:138: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py:140: job.status = "failed" /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:56:class PeerResponse(BaseModel): /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:60: public_key: str /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:62: paid_until: datetime /opt/wg-access/backend/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253:80: status: str /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:18: raise HTTPException(status_code=, detail="invalid agent token" /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:27: status: str /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:49: .where(ProvisioningJob.status == "pending") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:64: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:65: if job.status != "pending": /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:66: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:68: job.status = "running" /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:85: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:86: if job.status not in ("pending", "running"): /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:87: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:89: job.status = "completed" /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:107: raise HTTPException(status_code=, detail="job not found") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:108: if job.status not in ("pending", "running"): /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:109: raise HTTPException(status_code=, detail=f"job status is {job.status}") /opt/wg-access/backend/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954:111: job.status = "failed" === existing smoke dirs/files relevant === /opt/wg-access/tmp/mgts-smoke-20260706-183921/create.json /opt/wg-access/tmp/mgts-smoke-20260706-183921/peers-before.json /opt/wg-access/tmp/mgts-smoke-20260706-184120/allowed-ips-after-cancel.txt /opt/wg-access/tmp/mgts-smoke-20260706-184120/allowed-ips-after-enable.txt /opt/wg-access/tmp/mgts-smoke-20260706-184120/client.conf /opt/wg-access/tmp/mgts-smoke-20260706-184120/client.mgts.conf /opt/wg-access/tmp/mgts-smoke-20260706-184120/create.json /opt/wg-access/tmp/mgts-smoke-20260706-184120/smoke.env === smoke scripts and status scripts heads === ### /usr/local/sbin/wg-access-expire-subscriptions.sh -rwxr-xr-x 1 root root 1247 Jul 5 10:19 /usr/local/sbin/wg-access-expire-subscriptions.sh #!/usr/bin/env bash set -euo pipefail URL="${WG_ACCESS_EXPIRE_URL:-http://127.0.0.1:18080/admin/maintenance/expire-subscriptions}" LOG_DIR="/opt/wg-access/maintenance-logs" LOCK="/run/wg-access-expire-subscriptions.lock" mkdir -p "$LOG_DIR" ts="$(date -Is)" out="$LOG_DIR/expire-subscriptions.$(date +%Y%m%d).log" { echo "== $ts expire-subscriptions ==" flock -n 9 || { echo "another expire-subscriptions run is already active" exit 0 } rm -f /tmp/wg-access-expire-subscriptions.out /tmp/wg-access-expire-subscriptions.err code=" curl -fsS \ -m 30 \ -X POST \ -o /tmp/wg-access-expire-subscriptions.out \ -w "%{http_code}" \ "$URL" 2>/tmp/wg-access-expire-subscriptions.err || true )" echo "http_code=" if [ -s /tmp/wg-access-expire-subscriptions.out ]; then cat /tmp/wg-access-expire-subscriptions.out echo fi if [ -s /tmp/wg-access-expire-subscriptions.err ]; then echo "stderr:" cat /tmp/wg-access-expire-subscriptions.err fi rm -f /tmp/wg-access-expire-subscriptions.out /tmp/wg-access-expire-subscriptions.err case "$code" 2*) exit 0 ;; *) echo "ERROR: expire-subscriptions failed"; exit 1 ;; esac } 9>"$LOCK" | tee -a "$out" === historical mgts smoke envs, keys only === ### /opt/wg-access/tmp/mgts-smoke-20260706-184120/smoke.env WORK= SUB_ID= PEER_ID= TUNNEL_IP= PUBLIC_KEY= === historical create/cancel/enable json summaries, no private config === ### /opt/wg-access/tmp/mgts-smoke-20260706-183921/create.json { "detail": [ { "type": "value_error", "loc": [ "body", "email" ], "msg": "value is not a valid email address: The part after the @-sign is a special-use or reserved name that cannot be used with email.", "input": "wg-paid-mgts-smoke-20260706-183921@example.invalid", "ctx": { "reason": "The part after the @-sign is a special-use or reserved name that cannot be used with email." } } ] } ### /opt/wg-access/tmp/mgts-smoke-20260706-183921/peers-before.json [ { "id": "0f808400-74ad-437b-a716-c6389aa3f057", "user_id": "2cc7ce32-acc1-4258-92c6-bccd2febb7b6", "node_id": "ddn-test", "public_key": "Y9RXkNshIhljMAjSX9nyGZQLephRVV0TfIZfMhFn6R0=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-07-03T10:37:18.258073Z", "enabled": false, "created_at": "2026-07-05T10:37:15.949762Z" }, { "id": "28aefe9e-25b0-434a-aedc-a4df15609e98", "user_id": "a1957370-4921-4639-b4a9-6e6e05ab8c95", "node_id": "ddn-test", "public_key": "4U6GMvepAeuuXDaGryZQ9RSeomQfBCBbuKRdvNqbIQw=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-07-03T10:35:48.458083Z", "enabled": false, "created_at": "2026-07-05T10:35:47.463427Z" }, { "id": "40b76a1d-e1ef-4a26-ac11-f39043a6a190", "user_id": "07f03547-19ee-4068-ba4b-3ba504490337", "node_id": "ddn-test", "public_key": "7unzXkfaw3lD0tiSVT9UlMWesbD2OrzuT/CwLc10klE=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-07-03T10:30:53.275935Z", "enabled": false, "created_at": "2026-07-05T10:30:52.304156Z" }, { "id": "f45a349d-25a7-426d-b88a-4479b28aec18", "user_id": "7c1eb9f8-f220-43ae-840b-14f4cb362c1e", "node_id": "ddn-test", "public_key": "239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-07-04T18:20:43.380875Z", "enabled": false, "created_at": "2026-07-04T18:21:22.899633Z" }, { "id": "3460b486-358c-44d6-8f47-c960d6254e3a", "user_id": "1b42ec72-1f39-483d-ae1e-85669d3ecad2", "node_id": "ddn-test", "public_key": "dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-08-03T18:07:51.577444Z", "enabled": false, "created_at": "2026-07-04T18:07:51.596325Z" }, { "id": "486ee88e-2515-4203-8541-44f89cdaba89", "user_id": "cd6b00a0-8cc5-48df-8413-8e645505db8c", "node_id": "ddn-test", "public_key": "WkjrGK7JKsYkhzFz5Bggunzlw5BNY+WxkcYp4D7IulA=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-08-03T17:28:05.654224Z", "enabled": false, "created_at": "2026-07-04T17:28:05.667811Z" }, { "id": "da134a09-caa8-4209-bbfa-93dd06f310fb", "user_id": "ead942f1-f354-496a-b2e6-857604fea5c3", "node_id": "ddn-test", "public_key": "OtlMSSUslhQufF939tNN3quNVY0Y+bqmfyohE1K6sBE=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T17:22:24.372601Z", "enabled": false, "created_at": "2026-07-04T17:22:24.373856Z" }, { "id": "966f8262-b25c-4a13-ae1d-694b0378910a", "user_id": "2096c7ba-76f1-4c39-8bb4-53d629e23387", "node_id": "ddn-test", "public_key": "lAQxl4ykPcSpXxFNfz+pGS5Pn+im/uXlVmc+fd3UPB8=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T16:38:53.198957Z", "enabled": false, "created_at": "2026-07-04T16:38:53.204965Z" }, { "id": "00fde00e-a169-4397-841f-5cc99de475d5", "user_id": "3303b0a8-450a-4154-9322-9ddfc5757208", "node_id": "ddn-test", "public_key": "H+yLpZTzxVq4o9RSD6b70hu6JTIY5WkH6Tx74qP1dwM=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T16:32:01.766833Z", "enabled": false, "created_at": "2026-07-04T16:32:01.782547Z" }, { "id": "267835b6-84b6-4527-bd47-eee963e7a5c9", "user_id": "6a7eeddb-d012-403c-bf31-ea29a82c1d13", "node_id": "ddn-test", "public_key": "gXUwVRUraYFaUDv1BJ6GdxbZi6IoliYNBUe8wYRNERU=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T16:19:03.151534Z", "enabled": false, "created_at": "2026-07-04T16:19:03.152686Z" }, { "id": "6a5f1655-8ebe-435e-bba6-1af2412d40c2", "user_id": "46a582e8-3822-4686-b9b9-8751061fde97", "node_id": "ddn-test", "public_key": "TuuRNB7urYUrpRq9gHqqMlqWfte0jSJooLXjZIIYKjs=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T16:01:57.735658Z", "enabled": false, "created_at": "2026-07-04T16:01:57.736841Z" }, { "id": "9298ec04-26cf-44a7-8079-7a13664612fa", "user_id": "9bc16072-f8e5-489f-8817-c6778237d6d0", "node_id": "ddn-test", "public_key": "CJMx+fPoiz5TUmUHKF7JcKo9IrxBZZMEDk7aS/Temwk=", "tunnel_ip": "10.253.1.10", "paid_until": "2026-09-02T15:56:30.168829Z", "enabled": false, "created_at": "2026-07-04T15:56:30.178510Z" }, { "id": "1b19ed83-1cad-4d57-be9f-fded5270213c", "user_id": "2109fe84-fde0-4856-8268-030a43112782", "node_id": "ddn-test", "public_key": "mh6GsB55dOEZZCxiWy5PHGxcNSRcBLLNAUfgq4NV3jg=", "tunnel_ip": "10.253.1.16", "paid_until": "2026-09-02T15:26:17.089248Z", "enabled": false, "created_at": "2026-07-04T15:26:17.090574Z" }, { "id": "13543224-39f7-4cfc-917d-66a16e2e0e55", "user_id": "d360d32e-c7db-4688-846b-ecef626f12db", "node_id": "ddn-test", "public_key": "cMqUn9gKKZo0oh7vNotjs7s/UtuKVbl081arn6BPKik=", "tunnel_ip": "10.253.1.15", "paid_until": "2026-09-02T15:17:46.085909Z", "enabled": false, "created_at": "2026-07-04T15:17:46. ### /opt/wg-access/tmp/mgts-smoke-20260706-184120/create.json { "user_id": "13783bf9-94bb-4c90-b1c6-685a956a5c43", "subscription_id": "8adb06a9-713a-46b0-8759-d11c28a797fc", "peer_id": "3d03a7a2-c009-48a4-b6ff-214326d26fc0", "job_id": "814ba665-f6e5-4ba4-b1ba-8af1ec4954e6", "node_id": "ddn-test", "tunnel_ip": "10.253.1.10", "paid_until": "2026-08-05T18:41:20.288079Z", "private_key": "", "public_key": "zUUEdc+Lac9C6fmMDVosuO3ZIYHIUDWi34UVgdjhbCU=", "preshared_key": "jDmX4j1ipVr4r45cRb4nbKaeBqQYQeeaCpsBG4U2sMQ=", "client_config": "" } === container backend introspection: routes/help/read-only === backend_container=wgaccess-backend ### backend cwd/files /app total 20 drwxr-xr-x 4 root root 4096 Jul 4 10:18 . drwxr-xr-x 1 root root 4096 Jul 6 18:51 .. drwxr-xr-x 4 root root 4096 Jul 4 10:18 alembic -rw-r--r-- 1 root root 604 Jul 4 10:18 alembic.ini drwxr-xr-x 7 root root 4096 Jul 4 16:29 app /app/alembic/versions/0001_initial_schema.py /app/app/api/admin.py /app/app/api/admin.py.BEFORE_DISABLE_PEER.20260704-151253 /app/app/api/agent.py /app/app/api/agent.py.BEFORE_AGENT_PEERS_SYNC.20260704-133954 /app/app/api/dev.py /app/app/api/dev.py.BEFORE_CLIENT_CONFIG_RESPONSE.20260704-151636 /app/app/api/dev.py.BEFORE_REAL_WG_KEYS.20260704-132545 /app/app/models/__init__.py /app/app/models/core.py /app/app/services/wireguard.py ### backend python route list attempt MODULE app.main OK ROUTER admin_router ROUTER dev_router ROUTER agent_router MODULE app.api.admin OK ROUTER APIRouter ROUTER router MODULE app.api OK ### backend local health from inside container URL http://127.0.0.1:8000/health {"status":"ok","service":"wg-access-backend","environment":"dev"} URL http://0.0.0.0:8000/health {"status":"ok","service":"wg-access-backend","environment":"dev"} URL http://localhost:8000/health {"status":"ok","service":"wg-access-backend","environment":"dev"} ### backend openapi paths from inside container URL http://127.0.0.1:8000/openapi.json /admin/invites ['get', 'post'] /admin/jobs ['get'] /admin/maintenance/expire-subscriptions ['post'] /admin/peers ['get'] /admin/peers/{peer_id}/disable ['post'] /admin/subscriptions ['post'] /admin/subscriptions/{subscription_id}/cancel ['post'] /admin/users ['get'] /admin/users/by-email/{email} ['get'] /admin/users/{user_id} ['get'] /agent/jobs ['get'] /agent/jobs/{job_id}/complete ['post'] /agent/jobs/{job_id}/fail ['post'] /agent/jobs/{job_id}/start ['post'] /agent/peers ['get'] /dev/create-test-peer ['post'] URL http://localhost:8000/openapi.json /admin/invites ['get', 'post'] /admin/jobs ['get'] /admin/maintenance/expire-subscriptions ['post'] /admin/peers ['get'] /admin/peers/{peer_id}/disable ['post'] /admin/subscriptions ['post'] /admin/subscriptions/{subscription_id}/cancel ['post'] /admin/users ['get'] /admin/users/by-email/{email} ['get'] /admin/users/{user_id} ['get'] /agent/jobs ['get'] /agent/jobs/{job_id}/complete ['post'] /agent/jobs/{job_id}/fail ['post'] /agent/jobs/{job_id}/start ['post'] /agent/peers ['get'] /dev/create-test-peer ['post'] === database table names/counts via backend env, read-only if possible === db_url_present True len 79 db_readonly_failed ProgrammingError('missing "=" after "postgresql+psycopg://wg_access:fqoFHf2k4AqCh1OEwYjAp6Uv@postgres:5432/wg_access" in connection info string\n') === recent status files relevant heads === ### /opt/wg-access/STATUS-STEP-021C_ADMIN_SUBSCRIPTION_FLOW_OK.txt WG Access Dev checkpoint STEP: 021C STATUS: OK Admin/manual subscription endpoint smoke test completed successfully. New endpoint: POST /admin/subscriptions Implemented in: backend/app/api/admin.py Shared WireGuard helpers: backend/app/services/wireguard.py Endpoint behavior: Creates User Creates active Subscription Creates enabled Peer Creates pending ProvisioningJob enable_peer Returns ready WireGuard client_config once Backend status: /health OK OpenAPI lists: POST /admin/subscriptions Test subscription: user_id: 3303b0a8-450a-4154-9322-9ddfc5757208 subscription_id: 54b8c5b7-430d-425a-8fc0-909eeb0cab9d peer_id: 00fde00e-a169-4397-841f-5cc99de475d5 enable_job_id: bb71b4cd-a615-45f5-9135-4aa0efca9cb7 node_id: ddn-test tunnel_ip: 10.253.1.10 public_key: H+yLpZTzxVq4o9RSD6b70hu6JTIY5WkH6Tx74qP1dwM= plan_code: months: 2 auto_renew: false Timer provision: Agent was NOT run manually. wg-access-agent.timer processed enable_peer automatically. enable job status: completed attempts: 1 Client config test: saved as /etc/wireguard/wg-step021c.conf wg-quick up wg-step021c: OK ping 10.253.1.1: 4 packets transmitted 4 received 0% packet loss VM100 handshake: endpoint: 192.168.30.83:42824 latest handshake: OK allowed ips: 10.253.1.10/32 Cleanup: local wg-step021c interface was brought down. peer disabled through existing API: POST /admin/peers/{peer_id}/disable Disable job: disable_job_id: 6e273fdf-781f-4992-ad42-4bf83bc7b6f5 status: completed attempts: 1 processed automatically by wg-access-agent.timer Final state: Backend enabled peers count: 0 VM100 wg_paid: address: 10.253.1.1/16 route: 10.253.0.0/16 dev wg_paid runtime peers: none wg-access-agent.timer: enabled active (waiting) next trigger scheduled Current state: Backend clean. VM100 wg_paid clean. Timer active. /admin/subscriptions is ready as first non-dev manual subscription creation endpoint. ### /opt/wg-access/STATUS-STEP-025A_CANCEL_SUBSCRIPTION_OK.txt WG Access Dev checkpoint STEP: 025A STATUS: OK Admin cancel subscription endpoint added and smoke-tested. Changed: backend/app/api/admin.py New endpoint: POST /admin/subscriptions/{subscription_id}/cancel Endpoint behavior: Finds subscription by ID. Sets: subscription.status = canceled subscription.auto_renew = false subscription.canceled_at = now, if not already set Finds all enabled peers for subscription.user_id. For each enabled peer: peer.enabled = false peer.disabled_at = now creates pending disable_peer ProvisioningJob Returns: subscription_id user_id status canceled_at disabled_peer_count job_ids Backend status: /health OK OpenAPI routes verified: POST /admin/subscriptions POST /admin/subscriptions/{subscription_id}/cancel GET /admin/users GET /admin/users/{user_id} GET /admin/users/by-email/{email} Clean precheck: Backend enabled peers count: 0 Backend active pending/running jobs count: 0 Cancel smoke test subscription: user_id: 1b42ec72-1f39-483d-ae1e-85669d3ecad2 subscription_id: 26794caf-ab42-4cb7-8270-5e87e7b1d47d peer_id: 3460b486-358c-44d6-8f47-c960d6254e3a enable_job_id: 74ccc2cf-bbb2-4621-8bbe-ce3096d8366d node_id: ddn-test tunnel_ip: 10.253.1.10 public_key: dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg= Timer provision: Agent was NOT run manually. wg-access-agent.timer processed enable_peer automatically. enable job status: completed VM100 before cancel: peer appeared on wg_paid: public_key: dYuBNktWxl/PIqV49T9BIiHIvP4ZumswWZIEFD/Cwxg= allowed ips: 10.253.1.10/32 Cancel response: subscription_id: 26794caf-ab42-4cb7-8270-5e87e7b1d47d user_id: 1b42ec72-1f39-483d-ae1e-85669d3ecad2 status: canceled canceled_at: 2026-07-04T18:08:12.285041Z disabled_peer_count: 1 job_ids: 8aa5d01c-1a51-48e9-8b86-834f379e829d User detail after cancel: subscription: status: canceled auto_renew: false canceled_at set peer: enabled: false disabled_at set Timer deprovision: Agent was NOT run manually. wg-access-agent.timer processed cancel-created disable_peer job automatically. disable job status: completed attempts: 1 Idempotency: Second POST /admin/subscriptions/{subscription_id}/cancel returned: status: canceled disabled_peer_count: 0 job_ids: [] Final state: Backend enabled peers count: 0 Backend active pending/running jobs count: 0 VM100 wg_paid: address: 10.253.1.1/16 route: 10.253.0.0/16 dev wg_paid runtime peers: none wg-access-agent.timer: enabled active (waiting) next trigger scheduled Current state: Backend clean. VM100 wg_paid clean. Timer active. Admin can now create, inspect, and cancel subscriptions. ### /opt/wg-access/STATUS-STEP-026A_EXPIRE_SUBSCRIPTIONS_OK.txt WG Access Dev checkpoint STEP: 026A STATUS: OK Expiration maintenance endpoint added and smoke-tested. Changed: backend/app/api/admin.py New endpoint: POST /admin/maintenance/expire-subscriptions Endpoint behavior: Finds active subscriptions where paid_until <= now. For each expired subscription: subscription.status = expired subscription.auto_renew = false Finds all enabled peers for subscription.user_id. For each enabled peer: peer.enabled = false peer.disabled_at = now creates pending disable_peer ProvisioningJob Returns: expired_subscription_count disabled_peer_count job_ids subscription_ids Backend status: /health OK OpenAPI routes verified: POST /admin/maintenance/expire-subscriptions POST /admin/subscriptions POST /admin/subscriptions/{subscription_id}/cancel GET /health Clean precheck: Backend enabled peers count: 0 Backend active pending/running jobs count: 0 Expiration smoke test subscription: user_id: 7c1eb9f8-f220-43ae-840b-14f4cb362c1e subscription_id: 7a3dfe11-ebf3-4f5e-966b-73fdbe464426 peer_id: f45a349d-25a7-426d-b88a-4479b28aec18 enable_job_id: 838a2aaf-64ed-4346-9996-1ef7889fda2f node_id: ddn-test tunnel_ip: 10.253.1.10 public_key: 239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE= Timer provision: Agent was NOT run manually. wg-access-agent.timer processed enable_peer automatically. enable job status: completed VM100 before expiration: peer appeared on wg_paid: public_key: 239TtHnxaJefv7kKJeNOVjW6YLchjRcTdctI3nJrAXE= allowed ips: 10.253.1.10/32 Forced expiration: subscriptions.paid_until moved into the past peers.paid_until moved into the past Expire endpoint response: expired_subscription_count: 1 disabled_peer_count: 1 job_ids: f6431127-27e3-49fe-9da0-ef03fc52c2d9 subscription_ids: 7a3dfe11-ebf3-4f5e-966b-73fdbe464426 User detail after expiration: subscription: status: expired auto_renew: false paid_until in the past peer: enabled: false disabled_at set Timer deprovision: Agent was NOT run manually. wg-access-agent.timer processed expiration-created disable_peer job automatically. disable job status: completed attempts: 1 Idempotency: Second POST /admin/maintenance/expire-subscriptions returned: expired_subscription_count: 0 disabled_peer_count: 0 job_ids: [] subscription_ids: [] Final state: Backend enabled peers count: 0 Backend active pending/running jobs count: 0 VM100 wg_paid: address: 10.253.1.1/16 route: 10.253.0.0/16 dev wg_paid runtime peers: none wg-access-agent.timer: enabled active (waiting) next trigger scheduled Current state: Backend clean. VM100 wg_paid clean. Timer active. Expiration logic exists as a manual maintenance endpoint. ### /opt/wg-access/STATUS-STEP-028A_MGTS_WG_PAID_E2E_OK.txt STEP_028A_MGTS_WG_PAID_E2E_OK Date: 2026-07-06 Project: WG Access / WG Paid / MGTS Summary: MGTS wg_paid production lifecycle and E2E routing smoke passed. Confirmed: - VM121 agent target updated to MGTS VM100: REMOTE_HOST=10.71.100.1 REMOTE_WG_INTERFACE=wg_paid - Agent SSH key works against MGTS VM100. - Backend production flow OK: POST /admin/subscriptions -> created subscription -> created peer -> created enable_peer job systemctl start wg-access-agent.service -> agent fetched enable_peer job -> agent executed runtime wg set on MGTS VM100 wg_paid -> peer appeared on MGTS VM100 wg_paid - Temporary client smoke OK: interface: wg-paid-test address: 10.253.1.10/32 endpoint used for internal smoke: 10.71.100.1:51830 - E2E checks OK: ping 10.253.1.1 from wg-paid-test: OK ping 77.88.8.8 from wg-paid-test: OK ping 1.1.1.1 from wg-paid-test: OK, short test had 25% loss ping 9.9.9.9 from wg-paid-test: OK, short test had 25% loss - VM100 PBR route decision OK: 1.1.1.1 from 10.253.1.10 -> table pbr_transit_vpn via 10.200.0.2 77.88.8.8 from 10.253.1.10 -> table pbr_transit_direct via 10.201.0.2 - Cleanup OK: POST /admin/subscriptions//cancel -> disabled_peer_count=1 -> disable_peer job created systemctl start wg-access-agent.service -> agent processed disable_peer -> peer removed from MGTS VM100 wg_paid Important fix applied: /opt/wg-access/.env updated from old DDN/home values to MGTS smoke values: WG_SERVER_PUBLIC_KEY=FD1J+KVZ965pCjgtO0YWxJsKANY9uz6v1P54JLkiZzY= WG_CLIENT_ENDPOINT=10.71.100.1:51830 WG_CLIENT_ALLOWED_IPS=0.0.0.0/0 Backend recreated/restarted and health returned OK. build_client_config() now generates MGTS server public key, internal smoke endpoint 10.71.100.1:51830, and AllowedIPs 0.0.0.0/0. Production TODO: Replace WG_CLIENT_ENDPOINT=10.71.100.1:51830 with real external MGTS DNS/IP endpoint after MGTS static IP is known. Deferred: Backend container dependencies are still technical debt: force-recreate exposes runtime dependency installation. Need persistent backend image/requirements/compose cleanup later. Next session first step: Before changing VM121 again, collect full inventory of VM121 configs/tools: /opt/wg-access, agent env/service/key paths, backend .env, compose files, maintenance/timers, smoke dirs, status/checkpoint files. === STEP_034B RESULT === remote_rc=0 create_endpoint_related_lines=94 mgts_smoke_related_lines=14 backend_container_related_lines=2 db_table_lines=0 db_count_lines=0 redacted_lines=40 decision=PASS_VM121_PEER_TOOLING_INSPECT