Zhejianglab
Scope
ZJLAB operational inventory and network details are private. Public pages contain reusable application guidance only; verify dynamic state against the live cluster before applying a runbook.
Access
ssh zjlab-ubuntu-local hostname # when running on ZJLAB
ssh zjlab-ubuntu-local 'kubectl get nodes'
ssh zjlab-ubuntu-proxy hostname # when running on 72602Use zjlab-ubuntu-local for direct access from ZJLAB and
zjlab-ubuntu-proxy for the forwarded path from 72602. These are SSH config
aliases, not DNS names: validate configuration with ssh -G and reachability
with SSH, not with getent hosts. The aliases are provisioned from private
inventory and use an ECS ProxyJump to loopback-only reverse SSH listeners. Do
not publish their resolved endpoints, ports, users, internal topology, or
service names.
Detailed inventory and tunnel recovery procedures are maintained in the private ops-private repository with SOPS-encrypted values.
Tunnel Health
ECS runs an independent check-and-alert-only monitor for the approved
primary and backup loopback listeners. For each label it requires exactly
one listener, loopback-only binding, sole sshd ownership, an independent
owner, a short-timeout SSH banner, and a stable hashed owner signature. Alerts
start only after ten consecutive failures.
The monitor stores root-only state and emits labels and fixed reason codes only. It never restarts or kills a tunnel and never changes sshd, firewall, cloud network policy, DNS, keys, or endpoints. Real configuration and rollback details remain in the private SOPS inventory; public pages must not reproduce them.
The ZJLAB tunnel initiators use enabled system-level services so they recover
after a host reboot. User-level legacy tunnel services must remain stopped and
disabled; Linger=yes starts the user manager but does not start disabled
services. If both labels fail together, inspect host boot and service-manager
state first, then recover backup before primary. Verify each alias and its
loopback listener before proceeding to the next label.
For external availability history, report the existing primary and backup
check results to separate Uptime Kuma Push monitors. Do not expose the relay’s
loopback listeners just to make them reachable by Kuma. The Push URLs and the
ECS monitor configuration belong in the private SOPS inventory; see the
Uptime Kuma setup
for the public setup requirements.
The ECS-side integration is active: ECS listener 10023 reports the primary
check and listener 10024 reports the backup check. This is separate from
the 72602 public tunnel listeners on 10021 and 10022, which are not Kuma
Push sources for this monitor.
The stable access map is:
10023is the ZJLAB primary SSH reverse listener.10024is the backup listener.10025is a restricted break-glass maintenance listener.
All three are loopback-only on the ECS host and are reached through the ECS
ProxyJump path. They must never be added to the ECS security group or exposed
publicly. The maintenance listener is provided by
zjlab-loopback-maintenance.service and uses a separate restricted SSH
account and key; use the local zjlab-maintenance alias only for recovery.
Client configurations should use HostName 127.0.0.1 for these ECS loopback
targets rather than relying on localhost address-family resolution.
ECS applies ClientAliveInterval 30 and ClientAliveCountMax 10 to the
zjlab-tunnel and zjlab-maintenance accounts. This releases stale remote
forward listeners quickly after a broken connection without changing the
global SSH policy.
The ZJLAB tunnel initiators are the system-level services located at
/etc/systemd/system/zjlab-loopback-reverse-primary.service and
/etc/systemd/system/zjlab-loopback-reverse-backup.service. These units invoke
/usr/bin/ssh directly (not autossh) with Restart=on-failure,
RestartSec=30s for the primary, RestartSec=45s for the backup, and
KillMode=control-group. Legacy user-level services
reverse-tunnel-ecs.service and reverse-tunnel-ecs-10024.service must remain
stopped and disabled; running them alongside the system units causes listener
ownership conflicts and reconnect loops.
When both monitors report ssh_banner_failed, first verify the current ZJLAB
egress IPv4 and the ECS security-group allowlist for TCP 22, then check ECS
sshd and the established SSH child/session. A systemd active state alone only
proves that the supervisor is running; it does not prove that the reverse
listener has been established.
The private checker keeps the Push request as a best-effort reporting path.
Healthy checks send up; failed checks send down with a fixed reason. Push
HTTP failures do not change listener judgment, failure counters, DingTalk
debounce, or tunnel lifecycle. The checker merges existing query parameters
when constructing the request and does not log the Push value. A normal tunnel
reconnect changes the sshd PID and socket identity; those changes are not
treated as failures when the listener, owner, bind address, and SSH banner are
healthy. The checker completes an SSH key exchange with ssh-keyscan before
closing its probe connection; do not replace it with a raw TCP banner read,
which can leave CLOSE-WAIT channels on the reverse listener.
DingTalk sends one failure notification after ten consecutive failures for a label. The message includes a fixed, redacted recovery action. After a notified failure becomes healthy, the checker sends one recovery notification; if that send fails, the alert state is retained and the next healthy check retries it. Short failures that never cross the alert threshold do not generate a recovery message.
Deployment verification on 2026-09-05 confirmed the primary, backup, and
maintenance aliases returned zjlab-ubuntu; the ECS listeners were unique,
loopback-only, and returned valid SSH banners. The checker dry-run and live
service run were healthy for both labels across more than two complete
60-second timer cycles.
Rollback restores the root-only ECS backup and encrypted private inventory
backup, then restarts only zjlab-tunnel-healthcheck.service; tunnel units are
not restarted as part of monitoring rollback.
Preflight
For host-level proxy checks, follow the
shared Clash/Mihomo runbook before trying ports or changing
proxy variables. The installed clashctl is a shell function, so
non-interactive sessions must source it explicitly.
# On ZJLAB, use zjlab-ubuntu-local; on 72602, use zjlab-ubuntu-proxy.
ZJLAB_SSH_ALIAS=zjlab-ubuntu-local
ssh "$ZJLAB_SSH_ALIAS" 'kubectl config current-context'
ssh "$ZJLAB_SSH_ALIAS" 'kubectl get nodes'
ssh "$ZJLAB_SSH_ALIAS" 'kubectl get namespace'
ssh "$ZJLAB_SSH_ALIAS" 'kubectl get applications.argoproj.io -A'
ssh "$ZJLAB_SSH_ALIAS" 'kubectl get ingress,certificate -A'Independent Prometheus Deployment Attempt
An independent Prometheus deployment was prepared for ZJLAB with the public
Prometheus Community prometheus chart version 29.18.0, ArgoCD Application
zjlab-prometheus, and destination namespace monitoring. Helm rendering and
Kubernetes server-side dry-run succeeded, including the password-file based
remote-write mount and explicit kube-state-metrics and node-exporter scrape
jobs.
The Application reached Synced but remained Healthy: Progressing. The
30Gi local-path PVC stayed Pending: the local-path provisioner timed out
while creating its helper Pod and did not bind the volume. Consequently the
Prometheus server, kube-state-metrics, and node-exporter workloads did not
reach the verification gate, so readiness, targets, remote-write delivery,
and the receiving-cluster queries were not claimed as successful.
The attempt was rolled back by deleting only the newly created
zjlab-prometheus Application, its monitoring/zjlab-prometheus-server PVC,
the runtime remote-write Secret, and the empty monitoring namespace. Existing
applications, tunnels, network policies, and metrics-server were not changed.
Before retrying, verify local-path provisioning on the selected node and
repeat the full readiness, target, remote-write, and receiving-cluster query
checks.
Prometheus Retry (2026-08-05)
The retry used a private node selector for the previously diagnosed healthy
local-path node. The 30Gi PVC bound immediately, and the Prometheus server was
scheduled on the same node. Kube-state-metrics and both node-exporter Pods
reached Running.
The Prometheus server then exited with code 2 and entered CrashLoopBackOff.
This is a stop condition, so readiness, targets, remote-write queue health,
and receiving-cluster queries were not claimed as successful. The Application,
PVC, runtime Secret, and monitoring namespace created for this retry were
rolled back. Existing applications, tunnels, network policies, and
metrics-server were not changed. The next retry must capture the Prometheus
startup error before cleanup and correct the rendered configuration without
publishing credentials.
Prometheus Deployment (2026-08-05)
The deployment was recreated as ArgoCD Application zjlab-prometheus using
Prometheus Community chart 29.18.0. The rendered server arguments contain the
chart-default web.enable-lifecycle flag exactly once; no custom
server.extraArgs was configured. The runtime remote-write Secret contains
only the password file and is mounted read-only; credentials are not part of
the Application values.
ArgoCD is Synced and Healthy at revision 29.18.0. The 30Gi local-path
PVC is Bound, the Prometheus server is co-located with that volume on the
private selected node, the Prometheus and kube-state-metrics Deployments are
1/1 Ready, and both node-exporter DaemonSet Pods are Ready with zero restarts.
The server Service and the KSM/node-exporter Services are ClusterIP; no
Ingress, NodePort, or admin API was enabled.
Prometheus /-/ready returned HTTP 200. The targets API showed the Prometheus,
kube-state-metrics, node-exporter, Kubernetes API server, nodes, cadvisor, and
Kubernetes pod/service discovery jobs up with no scrape errors. The rendered
configuration contains external_labels.cluster=zjlab, the remote-write
password-file path, and no password value. Across two complete 30-second
cycles, remote-write samples increased while failed and retried samples stayed
at zero.
The receiving 72602 Prometheus queries were completed after deployment:
up{cluster="zjlab"} returned 18 samples,
kube_node_status_condition{cluster="zjlab"} returned 54 samples, and
kube_pod_info{cluster="zjlab"} returned 72 samples. The newest ZJLAB sample
was approximately five seconds old at verification time, and all three
queries were served by the existing 72602 Prometheus datasource used by
Grafana. No Grafana datasource change was required.
The receiving Prometheus reported zero failed and retried remote-write samples.
After the user-authorized 2026-08-14 metrics reset, the local
monitoring/zjlab-prometheus-server TSDB PVC was recreated while this
remote-write configuration remained unchanged. The old local history was not
backed up and is intentionally unrecoverable. The sender queue drained and the
72602 receiver again exposed fresh cluster="zjlab" samples; continue watching
the pending queue if its delay grows.
Prometheus Storage Diagnosis (2026-08-05)
The read-only follow-up found two Ready nodes with no taints and with
MemoryPressure, DiskPressure, and PIDPressure all false. The default
local-path StorageClass uses rancher.io/local-path, WaitForFirstConsumer,
and Delete reclaim policy. Its Rancher v0.0.35 provisioner is 1/1
Ready. The provisioner configuration has only the default root
/var/lib/rancher/k3s/storage; its helper image is BusyBox 1.37.0, and the
configured setup creates the volume directory and restricts its parent.
Existing Bound local-path volumes cover both nodes, and the node filesystem
and inode checks showed substantial headroom. Existing local-path mounts were
also writable from their consuming workloads. This does not support a root
capacity or permission failure as the cause of the Prometheus incident.
The retained helper events use the
helper-pod-create-pvc-<PVC-UID> naming pattern. The image was already
available, but helper container creation/startup and retries exceeded the
provisioner’s 120-second create-process timeout. Later retries included
ContainerCreating and failed log-stream reads before the Prometheus
resources were removed. No helper Pod, PVC, PV, or Prometheus Application is
currently left behind. The evidence points first to helper scheduling/runtime
latency or a local-path/K3s compatibility issue on the selected worker; the
retained evidence does not prove a filesystem fault.
nfs-data is currently 1/1 Ready with the v4.0.2 provisioner (Helm chart
4.0.18), an active leader endpoint, Immediate binding, Retain reclaim
policy, and archiveOnDelete=true. Multiple existing nfs-data PVCs are
Bound, and the NFS mount is currently read-write with healthy capacity and
inode headroom. It remains a single-backend/single-node dependency, so NFS is
an acceptable fallback for Prometheus only after confirming its latency and
failure policy; it is not automatic high availability.
The minimum reliable retry is to repair and verify helper startup on a deliberately selected, healthy local-path node, and to make the Prometheus workload’s node selection explicit so the local volume and workload remain co-located. Rollback is to remove only the new ArgoCD Application and its new resources; do not remove existing local-path data. NFS is the secondary option, and ephemeral storage is not recommended for Prometheus. Direct worker-host journal and host-permission inspection remains a verification gap because this diagnostic session was restricted to the approved canonical ZJLAB SSH entry point and did not create a debug Pod.
Prometheus Storage Follow-up (2026-08-05)
The approved node proxy exposed host system.journal and container-log
directories for both nodes. The historical helper had already been removed,
and no matching kubelet, containerd, CRI, mount, image, or sandbox error was
retained for the helper timeout window. The control-plane journal did contain
repeated PartialObjectMetadata watch errors, but no evidence connected them
to the local-path helper failure. The Kubernetes node image inventory no
longer advertises the helper image; the historical kubelet event is the
available evidence that it was already cached during the incident.
The strongest retry candidate is the control-plane node that already hosts the
local-path provisioner and several successfully Bound local-path volumes. It
has the standard Linux and control-plane/etcd labels, no taint, and healthy
Ready/pressure conditions. The trade-off is that Prometheus would share the
node with control-plane workloads, and loss of that node makes its local data
unavailable. Use the private value of its kubernetes.io/hostname label in
the deployment; do not publish that value.
For a retry, the server values should include the following shape, with
<selected-local-path-node> replaced only in the private manifest:
server:
nodeSelector:
kubernetes.io/hostname: <selected-local-path-node>
persistentVolume:
enabled: true
storageClass: local-path
accessModes:
- ReadWriteOnce
size: 30GiThe retry gate is: the PVC must reach Bound and receive node affinity for
the selected node before 120 seconds; the helper must reach container
started/completed without ContainerCreating, mount, image, sandbox, or CRI
errors; and the Prometheus server must schedule on the same node and become
Ready. Any repeated runtime error, a helper still in ContainerCreating at
60 seconds, or another ProvisioningFailed at 120 seconds is a stop
condition. Roll back the new Application and its new resources, then use
nfs-data only after accepting its single-backend failure risk. No runtime or
storage change was made during this follow-up.