@le-space/rootfs 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -86,11 +86,13 @@ virt-customize \
86
86
  --copy-in "${APP_BINARY}:${GUEST_APP_DIR}" \
87
87
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap.sh:/usr/local/sbin" \
88
88
  --copy-in "${SCRIPT_DIR}/uc-go-peer-configure.sh:/usr/local/sbin" \
89
+ --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-deregister.py:/usr/local/sbin" \
89
90
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.py:/usr/local/sbin" \
90
91
  --copy-in "${SCRIPT_DIR}/uc-go-peer-autotls-refresh.py:/usr/local/sbin" \
91
92
  --copy-in "${SCRIPT_DIR}/uc-go-peer-describe.py:/usr/local/sbin" \
92
93
  --copy-in "${SCRIPT_DIR}/uc-go-peer-setup-server.py:/usr/local/sbin" \
93
94
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap.service:/etc/systemd/system" \
95
+ --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-deregister.service:/etc/systemd/system" \
94
96
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.service:/etc/systemd/system" \
95
97
  --copy-in "${SCRIPT_DIR}/uc-go-peer-bootstrap-refresh.timer:/etc/systemd/system" \
96
98
  --copy-in "${SCRIPT_DIR}/uc-go-peer-autotls-refresh.service:/etc/systemd/system" \
@@ -98,6 +100,7 @@ virt-customize \
98
100
  --run-command "chmod 0755 ${ROOTFS_CONTRACT_BINARY_PATH}" \
99
101
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap.sh" \
100
102
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-configure.sh" \
103
+ --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap-deregister.py" \
101
104
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-bootstrap-refresh.py" \
102
105
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-autotls-refresh.py" \
103
106
  --run-command "chmod 0755 /usr/local/sbin/uc-go-peer-describe.py" \
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+
12
+ try:
13
+ from eth_account import Account
14
+ from eth_account.messages import encode_defunct
15
+ except ImportError as error: # pragma: no cover - runtime dependency
16
+ raise SystemExit(
17
+ "eth-account is required for guest-side bootstrap deregistration"
18
+ ) from error
19
+
20
+
21
+ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
22
+ DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-describe.py")
23
+ DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api2.aleph.im")
24
+ DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
25
+ DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
26
+ DEFAULT_POST_TYPE = os.environ.get("ALEPH_BOOTSTRAP_POST_TYPE", "relay-bootstrap")
27
+ MAX_PREVIOUS_PAGES = int(os.environ.get("ALEPH_BOOTSTRAP_MAX_PREVIOUS_PAGES", "5"))
28
+ PAGINATION = int(os.environ.get("ALEPH_BOOTSTRAP_PAGINATION", "50"))
29
+
30
+
31
+ def parse_env_file(path: str) -> dict[str, str]:
32
+ values: dict[str, str] = {}
33
+ if not os.path.exists(path):
34
+ return values
35
+
36
+ with open(path, encoding="utf-8") as handle:
37
+ for line in handle:
38
+ stripped = line.strip()
39
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
40
+ continue
41
+ key, value = stripped.split("=", 1)
42
+ values[key.strip()] = value.strip()
43
+ return values
44
+
45
+
46
+ def json_dumps(payload: object) -> str:
47
+ return json.dumps(payload, separators=(",", ":"))
48
+
49
+
50
+ def dedupe(values: list[str]) -> list[str]:
51
+ seen: set[str] = set()
52
+ result: list[str] = []
53
+ for value in values:
54
+ if value and value not in seen:
55
+ seen.add(value)
56
+ result.append(value)
57
+ return result
58
+
59
+
60
+ def sign_personal_message(private_key: str, payload: str) -> str:
61
+ message = encode_defunct(text=payload)
62
+ signed = Account.sign_message(message, private_key=private_key)
63
+ signature = signed.signature.hex()
64
+ return signature if signature.startswith("0x") else f"0x{signature}"
65
+
66
+
67
+ def address_from_private_key(private_key: str) -> str:
68
+ return Account.from_key(private_key).address
69
+
70
+
71
+ def signature_payload(chain: str, sender: str, message_type: str, item_hash: str) -> str:
72
+ return "\n".join([chain, sender, message_type, item_hash])
73
+
74
+
75
+ def post_json(url: str, body: dict[str, object]) -> tuple[int, dict]:
76
+ data = json_dumps(body).encode("utf-8")
77
+ request = urllib.request.Request(
78
+ url,
79
+ data=data,
80
+ headers={"content-type": "application/json"},
81
+ method="POST",
82
+ )
83
+ try:
84
+ with urllib.request.urlopen(request, timeout=30) as response:
85
+ payload = response.read().decode("utf-8")
86
+ return response.status, json.loads(payload or "{}")
87
+ except urllib.error.HTTPError as error:
88
+ payload = error.read().decode("utf-8")
89
+ try:
90
+ return error.code, json.loads(payload or "{}")
91
+ except json.JSONDecodeError:
92
+ return error.code, {"details": payload}
93
+
94
+
95
+ def get_json(url: str) -> dict:
96
+ with urllib.request.urlopen(url, timeout=30) as response:
97
+ return json.loads(response.read().decode("utf-8") or "{}")
98
+
99
+
100
+ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) -> dict[str, object]:
101
+ signed = dict(unsigned_message)
102
+ signed["signature"] = sign_personal_message(
103
+ private_key,
104
+ signature_payload(
105
+ str(unsigned_message["chain"]),
106
+ str(unsigned_message["sender"]),
107
+ str(unsigned_message["type"]),
108
+ str(unsigned_message["item_hash"]),
109
+ ),
110
+ )
111
+ return signed
112
+
113
+
114
+ def is_invalid_message_format(http_status: int, payload: dict) -> bool:
115
+ if http_status != 422:
116
+ return False
117
+ details = payload.get("details")
118
+ if isinstance(details, str) and "InvalidMessageFormat" in details:
119
+ return True
120
+ if isinstance(details, dict):
121
+ message = details.get("message")
122
+ if isinstance(message, str) and "InvalidMessageFormat" in message:
123
+ return True
124
+ return False
125
+
126
+
127
+ def is_retryable_broadcast_failure(http_status: int, payload: dict) -> bool:
128
+ if http_status >= 500:
129
+ return True
130
+ publication_status = payload.get("publication_status")
131
+ if isinstance(publication_status, dict):
132
+ status = publication_status.get("status")
133
+ if isinstance(status, str) and status.strip().lower() == "error":
134
+ return True
135
+ return False
136
+
137
+
138
+ def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
139
+ url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
140
+ attempts = [
141
+ {"sync": True, "message": message},
142
+ {**message, "sync": True},
143
+ dict(message),
144
+ ]
145
+ for index, attempt in enumerate(attempts):
146
+ http_status, payload = post_json(url, attempt)
147
+ if 200 <= http_status < 300:
148
+ return http_status, payload
149
+ can_retry = index < len(attempts) - 1 and (
150
+ is_invalid_message_format(http_status, payload)
151
+ or is_retryable_broadcast_failure(http_status, payload)
152
+ )
153
+ if not can_retry:
154
+ raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
155
+ raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
156
+
157
+
158
+ def parse_post_record(entry: object) -> dict[str, object] | None:
159
+ if not isinstance(entry, dict):
160
+ return None
161
+ item_hash = entry.get("item_hash") or entry.get("hash")
162
+ if not isinstance(item_hash, str) or not item_hash:
163
+ return None
164
+
165
+ sender = entry.get("address") or entry.get("sender")
166
+ item_content = entry.get("item_content")
167
+ if isinstance(item_content, str):
168
+ try:
169
+ item_content = json.loads(item_content)
170
+ except json.JSONDecodeError:
171
+ item_content = None
172
+
173
+ if not isinstance(item_content, dict):
174
+ return None
175
+
176
+ content = item_content.get("content")
177
+ if not isinstance(content, dict):
178
+ return None
179
+
180
+ return {
181
+ "item_hash": item_hash,
182
+ "sender": sender,
183
+ "registration_id": content.get("registrationId"),
184
+ "peer_id": content.get("peerId"),
185
+ }
186
+
187
+
188
+ def load_peer_id() -> str | None:
189
+ describe = subprocess.run([DESCRIBE_SCRIPT], check=False, capture_output=True, text=True)
190
+ if describe.returncode != 0:
191
+ return None
192
+ try:
193
+ metadata = json.loads(describe.stdout.strip() or "{}")
194
+ except json.JSONDecodeError:
195
+ return None
196
+ peer_id = metadata.get("peer_id")
197
+ if isinstance(peer_id, str) and peer_id.strip():
198
+ return peer_id.strip()
199
+ return None
200
+
201
+
202
+ def fetch_current_hashes(
203
+ api_host: str,
204
+ channel: str,
205
+ ref: str,
206
+ post_type: str,
207
+ sender: str,
208
+ registration_id: str | None,
209
+ peer_id: str | None,
210
+ ) -> list[str]:
211
+ found: list[str] = []
212
+ for page in range(1, MAX_PREVIOUS_PAGES + 1):
213
+ url = (
214
+ f"{api_host.rstrip('/')}/api/v0/posts.json?"
215
+ f"channels={urllib.parse.quote(channel)}&"
216
+ f"refs={urllib.parse.quote(ref)}&"
217
+ f"types={urllib.parse.quote(post_type)}&"
218
+ f"pagination={PAGINATION}&page={page}"
219
+ )
220
+ payload = get_json(url)
221
+ posts = payload.get("posts")
222
+ if not isinstance(posts, list):
223
+ break
224
+
225
+ for entry in posts:
226
+ parsed = parse_post_record(entry)
227
+ if parsed is None:
228
+ continue
229
+ if str(parsed["sender"]).lower() != sender.lower():
230
+ continue
231
+ if registration_id and parsed["registration_id"] == registration_id:
232
+ found.append(str(parsed["item_hash"]))
233
+ continue
234
+ if peer_id and parsed["peer_id"] == peer_id:
235
+ found.append(str(parsed["item_hash"]))
236
+
237
+ if len(posts) < PAGINATION:
238
+ break
239
+
240
+ return dedupe(found)
241
+
242
+
243
+ def broadcast_forget(
244
+ api_host: str,
245
+ sender: str,
246
+ private_key: str,
247
+ hashes: list[str],
248
+ channel: str,
249
+ ) -> tuple[int, dict] | None:
250
+ if not hashes:
251
+ return None
252
+
253
+ now_seconds = time.time()
254
+ item_content = json_dumps(
255
+ {
256
+ "address": sender,
257
+ "time": now_seconds,
258
+ "hashes": hashes,
259
+ "aggregates": [],
260
+ "reason": f"Deregister relay bootstrap records for {sender}",
261
+ }
262
+ )
263
+ unsigned = {
264
+ "sender": sender,
265
+ "chain": "ETH",
266
+ "type": "FORGET",
267
+ "item_hash": hashlib.sha256(item_content.encode("utf-8")).hexdigest(),
268
+ "item_type": "inline",
269
+ "item_content": item_content,
270
+ "time": now_seconds,
271
+ "channel": channel,
272
+ }
273
+ return broadcast_aleph_message(api_host, sign_aleph_message(unsigned, private_key))
274
+
275
+
276
+ def main() -> None:
277
+ env_values = parse_env_file(ENV_FILE)
278
+ publisher_private_key = env_values.get("ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY", "").strip()
279
+ registration_id = env_values.get("ALEPH_BOOTSTRAP_REGISTRATION_ID", "").strip() or None
280
+ if not publisher_private_key:
281
+ print(json_dumps({"status": "skipped", "reason": "missing publisher key"}))
282
+ return
283
+
284
+ publisher_address = address_from_private_key(publisher_private_key)
285
+ peer_id = load_peer_id()
286
+ channel = env_values.get("ALEPH_BOOTSTRAP_CHANNEL", DEFAULT_CHANNEL).strip() or DEFAULT_CHANNEL
287
+ ref = env_values.get("ALEPH_BOOTSTRAP_REF", DEFAULT_REF).strip() or DEFAULT_REF
288
+ post_type = env_values.get("ALEPH_BOOTSTRAP_POST_TYPE", DEFAULT_POST_TYPE).strip() or DEFAULT_POST_TYPE
289
+ api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
290
+
291
+ hashes = fetch_current_hashes(
292
+ api_host,
293
+ channel,
294
+ ref,
295
+ post_type,
296
+ publisher_address,
297
+ registration_id,
298
+ peer_id,
299
+ )
300
+ if not hashes:
301
+ print(json_dumps({"status": "skipped", "reason": "no matching bootstrap records"}))
302
+ return
303
+
304
+ http_status, response = broadcast_forget(
305
+ api_host, publisher_address, publisher_private_key, hashes, channel
306
+ ) or (0, {})
307
+ print(
308
+ json_dumps(
309
+ {
310
+ "status": "forgotten",
311
+ "httpStatus": http_status,
312
+ "sender": publisher_address,
313
+ "registrationId": registration_id,
314
+ "peerId": peer_id,
315
+ "forgottenHashes": hashes,
316
+ "response": response,
317
+ }
318
+ )
319
+ )
320
+
321
+
322
+ if __name__ == "__main__":
323
+ try:
324
+ main()
325
+ except Exception as error: # pragma: no cover - runtime error path
326
+ print(json_dumps({"status": "error", "error": str(error)}), file=sys.stderr)
327
+ raise
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=Best-effort Aleph bootstrap deregistration on shutdown for uc-go-peer
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+ ConditionPathExists=/etc/default/uc-go-peer.ready
6
+
7
+ [Service]
8
+ Type=oneshot
9
+ RemainAfterExit=yes
10
+ Environment=ENV_FILE=/etc/default/uc-go-peer
11
+ Environment=DESCRIBE_SCRIPT=/usr/local/sbin/uc-go-peer-describe.py
12
+ ExecStart=/bin/true
13
+ ExecStop=/usr/bin/python3 /usr/local/sbin/uc-go-peer-bootstrap-deregister.py
14
+ StandardOutput=journal
15
+ StandardError=journal
16
+ TimeoutStopSec=25
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target
@@ -117,7 +117,8 @@ def json_dumps(payload: object) -> str:
117
117
  def sign_personal_message(private_key: str, payload: str) -> str:
118
118
  message = encode_defunct(text=payload)
119
119
  signed = Account.sign_message(message, private_key=private_key)
120
- return signed.signature.hex()
120
+ signature = signed.signature.hex()
121
+ return signature if signature.startswith("0x") else f"0x{signature}"
121
122
 
122
123
 
123
124
  def address_from_private_key(private_key: str) -> str:
@@ -302,6 +303,30 @@ def sign_aleph_message(unsigned_message: dict[str, object], private_key: str) ->
302
303
  return signed
303
304
 
304
305
 
306
+ def is_invalid_message_format(http_status: int, payload: dict) -> bool:
307
+ if http_status != 422:
308
+ return False
309
+ details = payload.get("details")
310
+ if isinstance(details, str) and "InvalidMessageFormat" in details:
311
+ return True
312
+ if isinstance(details, dict):
313
+ message = details.get("message")
314
+ if isinstance(message, str) and "InvalidMessageFormat" in message:
315
+ return True
316
+ return False
317
+
318
+
319
+ def is_retryable_broadcast_failure(http_status: int, payload: dict) -> bool:
320
+ if http_status >= 500:
321
+ return True
322
+ publication_status = payload.get("publication_status")
323
+ if isinstance(publication_status, dict):
324
+ status = publication_status.get("status")
325
+ if isinstance(status, str) and status.strip().lower() == "error":
326
+ return True
327
+ return False
328
+
329
+
305
330
  def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[int, dict]:
306
331
  url = urllib.parse.urljoin(api_host.rstrip("/") + "/", "api/v0/messages")
307
332
  attempts = [
@@ -309,14 +334,17 @@ def broadcast_aleph_message(api_host: str, message: dict[str, object]) -> tuple[
309
334
  {**message, "sync": True},
310
335
  dict(message),
311
336
  ]
312
- last: tuple[int, dict] | None = None
313
- for attempt in attempts:
314
- last = post_json(url, attempt)
315
- if 200 <= last[0] < 300:
316
- return last
317
- if last is None:
318
- raise RuntimeError("Aleph broadcast failed before any attempt was made")
319
- raise RuntimeError(f"Aleph broadcast failed: {last[0]} {json_dumps(last[1])}")
337
+ for index, attempt in enumerate(attempts):
338
+ http_status, payload = post_json(url, attempt)
339
+ if 200 <= http_status < 300:
340
+ return http_status, payload
341
+ can_retry = index < len(attempts) - 1 and (
342
+ is_invalid_message_format(http_status, payload)
343
+ or is_retryable_broadcast_failure(http_status, payload)
344
+ )
345
+ if not can_retry:
346
+ raise RuntimeError(f"Aleph broadcast failed: {http_status} {json_dumps(payload)}")
347
+ raise RuntimeError("Aleph broadcast failed: no compatible request format was accepted")
320
348
 
321
349
 
322
350
  def parse_post_record(entry: object) -> dict[str, object] | None:
@@ -466,9 +494,6 @@ def main() -> None:
466
494
  version,
467
495
  now_ms,
468
496
  )
469
- if owner_authorization is None:
470
- print(json_dumps({"status": "skipped", "reason": "missing owner authorization"}))
471
- return
472
497
 
473
498
  proof_payload = relay_proof_payload(
474
499
  peer_id,
@@ -529,6 +554,7 @@ def main() -> None:
529
554
  "publishedMultiaddrs": multiaddrs,
530
555
  "publishedBrowserMultiaddrs": browser_multiaddrs,
531
556
  "forgottenHashes": previous_hashes,
557
+ "ownerAuthorizationPresent": owner_authorization is not None,
532
558
  "forgetResponse": forget_response[1] if forget_response else None,
533
559
  "response": response,
534
560
  }
@@ -11,6 +11,7 @@ SERVICE_NAME="${SERVICE_NAME:-uc-go-peer.service}"
11
11
  AUTOTLS_REFRESH_SERVICE="${AUTOTLS_REFRESH_SERVICE:-uc-go-peer-autotls-refresh.service}"
12
12
  BOOTSTRAP_REFRESH_TIMER="${BOOTSTRAP_REFRESH_TIMER:-uc-go-peer-bootstrap-refresh.timer}"
13
13
  BOOTSTRAP_REFRESH_SERVICE="${BOOTSTRAP_REFRESH_SERVICE:-uc-go-peer-bootstrap-refresh.service}"
14
+ BOOTSTRAP_DEREGISTER_SERVICE="${BOOTSTRAP_DEREGISTER_SERVICE:-uc-go-peer-bootstrap-deregister.service}"
14
15
  CADDY_SERVICE="${CADDY_SERVICE:-caddy.service}"
15
16
  CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
16
17
  BOOTSTRAP_SERVICE="${BOOTSTRAP_SERVICE:-uc-go-peer-bootstrap.service}"
@@ -97,6 +98,27 @@ current_identity_path() {
97
98
  printf '/var/lib/uc-go-peer/identity.key\n'
98
99
  }
99
100
 
101
+ current_bootstrap_publisher_private_key() {
102
+ local existing
103
+ existing="$(grep -E '^[#[:space:]]*ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY=' "${ENV_FILE}" 2>/dev/null | tail -n1 | cut -d= -f2- || true)"
104
+ if [ -n "${existing}" ]; then
105
+ printf '%s\n' "${existing}"
106
+ return
107
+ fi
108
+ printf '\n'
109
+ }
110
+
111
+ generate_bootstrap_publisher_private_key() {
112
+ while true; do
113
+ local candidate
114
+ candidate="$(openssl rand -hex 32)"
115
+ if [ -n "${candidate}" ] && [ "${candidate}" != "0000000000000000000000000000000000000000000000000000000000000000" ]; then
116
+ printf '0x%s\n' "${candidate}"
117
+ return
118
+ fi
119
+ done
120
+ }
121
+
100
122
  while [ "$#" -gt 0 ]; do
101
123
  case "$1" in
102
124
  --public-ipv4)
@@ -179,6 +201,13 @@ fi
179
201
  touch "${ENV_FILE}"
180
202
  rm -f "${AUTOTLS_READY_FILE}" "${AUTOTLS_ZONE_FILE}" "${AUTOTLS_HOSTS_FILE}" "${AUTOTLS_CADDY_READY_FILE}"
181
203
 
204
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
205
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(current_bootstrap_publisher_private_key)"
206
+ fi
207
+ if [ -z "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
208
+ BOOTSTRAP_PUBLISHER_PRIVATE_KEY="$(generate_bootstrap_publisher_private_key)"
209
+ fi
210
+
182
211
  announce=(
183
212
  "/ip4/${PUBLIC_IPV4}/tcp/${TCP_PORT}"
184
213
  )
@@ -234,6 +263,7 @@ if [ -n "${UDP_PORT}" ]; then
234
263
  fi
235
264
  if [ -n "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}" ]; then
236
265
  write_env_var "ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
266
+ write_env_var "GO_PEER_IDENTITY_EVM_PRIVATE_KEY" "${BOOTSTRAP_PUBLISHER_PRIVATE_KEY}"
237
267
  fi
238
268
  if [ -n "${BOOTSTRAP_PUBLISHER_LIBP2P_IDENTITY_B64}" ]; then
239
269
  identity_path="$(current_identity_path)"
@@ -261,6 +291,8 @@ if [ "${START_SERVICE}" -eq 1 ]; then
261
291
  systemctl restart "${SERVICE_NAME}"
262
292
  systemctl enable "${AUTOTLS_REFRESH_SERVICE}"
263
293
  systemctl enable "${BOOTSTRAP_REFRESH_TIMER}"
294
+ systemctl enable "${BOOTSTRAP_DEREGISTER_SERVICE}"
295
+ systemctl restart "${BOOTSTRAP_DEREGISTER_SERVICE}"
264
296
  systemctl start "${BOOTSTRAP_REFRESH_TIMER}"
265
297
  systemctl restart --no-block "${BOOTSTRAP_REFRESH_SERVICE}" || true
266
298
  if [ -n "${PROXY_HOSTNAME}" ]; then