@dooer/dooer-test-env 1.11.2 → 1.13.0

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.
@@ -0,0 +1,175 @@
1
+ # Getting access (new developer)
2
+
3
+ `dooer-test-env` reads the staging cluster to build its compose file, log in to the image registries and
4
+ fetch the BankID cert. That needs two things, in this order:
5
+
6
+ 1. **VPN** — the cluster API is not reachable from the open internet.
7
+ 2. **kubectl access** — a client certificate identifying you to the cluster.
8
+
9
+ **Both come from an admin**: they create your VPN tunnel and your cluster credential, and send you two
10
+ files — a WireGuard `.conf` and a `kubeconfig`. You install them and check they work. See
11
+ [issuing-access.md](./issuing-access.md) for their side.
12
+
13
+ ---
14
+
15
+ ## 0. Prerequisites
16
+
17
+ ```bash
18
+ brew install wireguard-tools kubernetes-cli
19
+ ```
20
+
21
+ `openssl` ships with macOS. Check everything is there:
22
+
23
+ ```bash
24
+ wg --version && kubectl version --client && openssl version
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 1. VPN
30
+
31
+ **Your admin creates the tunnel and sends you a `.conf` file.** You do not create it yourself and you do
32
+ not need a Robo10 token for this — ask an admin, who follows
33
+ [issuing-access.md § 1](./issuing-access.md#1-vpn--create-and-hand-over-a-tunnel).
34
+
35
+ The file they send you contains a **private key**. Treat it like a password: keep it out of chat, tickets
36
+ and git, install it, and delete the copy they sent you.
37
+
38
+ ### 1a. Install the config
39
+
40
+ `wg-quick` takes the interface name from the **filename**, so the name you pick here is the name you use in
41
+ every later command. Check first what is already there — **do not overwrite an existing tunnel**:
42
+
43
+ ```bash
44
+ ls /opt/homebrew/etc/wireguard/
45
+ ```
46
+
47
+ Then install it (using `roboten` as the interface name):
48
+
49
+ ```bash
50
+ sudo mkdir -p /opt/homebrew/etc/wireguard
51
+ sudo cp ~/Downloads/<the-file-they-sent>.conf /opt/homebrew/etc/wireguard/roboten.conf
52
+ sudo chmod 600 /opt/homebrew/etc/wireguard/roboten.conf
53
+ rm ~/Downloads/<the-file-they-sent>.conf # the copy under /opt/homebrew is the one wg-quick reads
54
+ ```
55
+
56
+ ### 1b. Connect, disconnect, restart
57
+
58
+ ```bash
59
+ sudo wg-quick up roboten # connect
60
+ sudo wg-quick down roboten # disconnect
61
+ sudo wg-quick down roboten && sudo wg-quick up roboten # restart (after a config change or sleep/wake)
62
+
63
+ sudo wg show # status: handshake time + transfer counters
64
+ ```
65
+
66
+ `wg-quick` is not a daemon: the tunnel does **not** survive a reboot, and after a laptop sleep it sometimes
67
+ needs the restart above. If `wg show` reports a `latest handshake` older than a couple of minutes while you
68
+ are actively using the cluster, restart it.
69
+
70
+ To start it automatically at boot:
71
+
72
+ ```bash
73
+ sudo brew services start wireguard-tools # starts wg0 by default
74
+ ```
75
+
76
+ That service is hard-wired to the `wg0` interface, so it only helps if you named your file `wg0.conf`.
77
+
78
+ ### 1c. Verify the VPN works
79
+
80
+ ```bash
81
+ sudo wg show # expect a recent "latest handshake"
82
+ nc -vz k8s.roboten-infra.com 64430 # expect: succeeded / open
83
+ ```
84
+
85
+ If `nc` hangs or is refused, the tunnel is not carrying traffic — restart it (1b). Until this passes, no
86
+ amount of kubectl configuration will help.
87
+
88
+ > The tunnel routes the cluster network (including the API server) through WireGuard. If you already have
89
+ > another VPN claiming those ranges, they will fight over the routes — bring only one up at a time.
90
+
91
+ ---
92
+
93
+ ## 2. kubectl access
94
+
95
+ **Your admin sends you a ready-made `kubeconfig` file.** You do not generate keys or assemble anything —
96
+ they create the credential and hand it over
97
+ ([issuing-access.md § 2](./issuing-access.md#2-kubectl--create-their-credential)).
98
+
99
+ Like the VPN config, that file contains a **private key**. Same handling: install it, then delete the copy
100
+ they sent you.
101
+
102
+ ### 2a. Install it
103
+
104
+ **If you have no other clusters** — simplest, just put it in place:
105
+
106
+ ```bash
107
+ mkdir -p ~/.kube
108
+ cp ~/Downloads/<yourname>.kubeconfig ~/.kube/config
109
+ chmod 600 ~/.kube/config
110
+ rm ~/Downloads/<yourname>.kubeconfig
111
+ ```
112
+
113
+ **If you already use kubectl for something else**, merge instead of overwriting:
114
+
115
+ ```bash
116
+ cp ~/.kube/config ~/.kube/config.backup # always back up first
117
+ KUBECONFIG=~/.kube/config:~/Downloads/<yourname>.kubeconfig \
118
+ kubectl config view --flatten > /tmp/merged && mv /tmp/merged ~/.kube/config
119
+ chmod 600 ~/.kube/config
120
+ rm ~/Downloads/<yourname>.kubeconfig
121
+ ```
122
+
123
+ Then select the context (the admin will tell you its name, `<firstname>-admin@kubernetes`):
124
+
125
+ ```bash
126
+ kubectl config get-contexts
127
+ kubectl config use-context <firstname>-admin@kubernetes
128
+ ```
129
+
130
+ ### 2b. Verify
131
+
132
+ With the VPN up:
133
+
134
+ ```bash
135
+ kubectl auth whoami
136
+ # Username <firstname>-admin
137
+ # Groups [system:authenticated]
138
+
139
+ kubectl get pods -n dooer-staging | head -3
140
+ kubectl get pods -n dooer-production | head -3
141
+ ```
142
+
143
+ Then the two checks `dooer-test-env` actually depends on:
144
+
145
+ ```bash
146
+ kubectl auth can-i get secrets -n dooer-staging # yes — registry pull secrets + BankID cert
147
+ kubectl get svc dooer-database -n dooer-staging # the database service the base-DB build reads
148
+ ```
149
+
150
+ All of those must succeed before `dooer-test-env setup` will work.
151
+
152
+ ---
153
+
154
+ ## 3. You are done
155
+
156
+ ```bash
157
+ npx @dooer/dooer-test-env@latest setup
158
+ ```
159
+
160
+ `setup` re-checks all of the above and tells you exactly which piece is missing if something is wrong.
161
+
162
+ ---
163
+
164
+ ## Troubleshooting
165
+
166
+ | Symptom | Cause |
167
+ | --- | --- |
168
+ | `dial tcp … i/o timeout` from kubectl | VPN is down — `sudo wg show`, then restart it (1b) |
169
+ | `nc` to the API server hangs | Tunnel is up but not carrying traffic; restart it |
170
+ | `error: You must be logged in to the server (Unauthorized)` | The certificate in your kubeconfig is expired or unknown to the cluster — ask for a re-issue |
171
+ | `Error from server (Forbidden)` | You *are* authenticated; your user has no permissions bound yet — ask the admin to complete their § 2e (bind permissions) |
172
+ | `wg-quick: 'roboten' already exists` | The interface is already up — `sudo wg-quick down roboten` first |
173
+
174
+ Certificates are issued for **one year**. When yours expires, ask your admin for a new kubeconfig and
175
+ repeat section 2 — your permissions stay in place, so they only have to re-issue the credential.
@@ -0,0 +1,271 @@
1
+ # Issuing access (admin)
2
+
3
+ The other side of [getting-access.md](./getting-access.md). Two independent things:
4
+
5
+ | | Who does it | What you do |
6
+ | --- | --- | --- |
7
+ | **VPN tunnel** | you | create it and send them the config |
8
+ | **kubectl credential** | you | create the certificate, bind permissions, send a kubeconfig |
9
+
10
+ You need a Robo10 token (§1a), `kubectl` with cluster-admin (`system:masters`), and your own VPN up.
11
+
12
+ ---
13
+
14
+ ## 1. VPN — create and hand over a tunnel
15
+
16
+ You create the tunnel and send the developer its config. They do not need a Robo10 token, which matters:
17
+ a brand-new joiner often has no working login yet, and the VPN API generates the keypair **server-side**
18
+ anyway — the config comes back with the private key in it either way, so there is no custody advantage to
19
+ making them do it.
20
+
21
+ ### 1a. Get a Robo10 token (yours)
22
+
23
+ Opens a browser — log in with **your `@robo10.com` account**:
24
+
25
+ ```bash
26
+ TOKEN="$(go run github.com/strehle/cmdline-openid-client/openid-client@latest \
27
+ -issuer https://oidc-provider-backend.api.staging.roboten-dev.com \
28
+ -client_id ro_authentication_cl_id \
29
+ -port 34466 | awk '/===/ { if (start) nextfile; start = 1; next } start { print }' | jq -r .access_token)"
30
+
31
+ [ -n "$TOKEN" ] && echo "token acquired" || echo "no token — did the browser login complete?"
32
+ ```
33
+
34
+ Needs `go` and `jq` (`brew install go jq`). Keep using **this same terminal**: `$TOKEN` lives only in this
35
+ shell, and everything below needs it.
36
+
37
+ <details>
38
+ <summary>Alternative if the Go client will not run</summary>
39
+
40
+ 1. Open <https://chat.roboten.com> and log in.
41
+ 2. DevTools → **Application** → **Storage → Cookies**.
42
+ 3. Copy the value of the `x-op-token` cookie.
43
+ 4. In your terminal: `TOKEN=<the-cookie-value>`
44
+ </details>
45
+
46
+ ### 1b. Create their tunnel
47
+
48
+ Name it after the person (and their machine if they need more than one) so the list stays readable:
49
+
50
+ ```bash
51
+ NAME="Firstname Lastname"
52
+ curl -sS -H "Authorization: Bearer $TOKEN" \
53
+ -H 'Content-Type: application/json' \
54
+ -d "{ \"name\": \"$NAME\" }" \
55
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels
56
+ ```
57
+
58
+ The response contains the tunnel's **id**. To find it again later, list them (1d).
59
+
60
+ ### 1c. Download the config and hand it over
61
+
62
+ ```bash
63
+ ID=<the id from the previous step>
64
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
65
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels/$ID/actions/get-config \
66
+ > ~/Downloads/$NAME.conf
67
+ ```
68
+
69
+ > **This file contains a private key.** Send it over a channel you would send a password over — a shared
70
+ > vault entry or an expiring secure link — not email, Slack or a ticket. Tell them to install it and delete
71
+ > the copy you sent, and delete your own copy once they confirm.
72
+
73
+ Then point them at [getting-access.md § 1](./getting-access.md#1-vpn).
74
+
75
+ One tunnel per person per machine: two devices sharing one config will fight over the session, so create a
76
+ second tunnel rather than re-sending the first.
77
+
78
+ ### 1d. List tunnels
79
+
80
+ ```bash
81
+ curl -sS -H "Authorization: Bearer $TOKEN" https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels
82
+ ```
83
+
84
+ ### 1e. Revoke a tunnel
85
+
86
+ `DELETE` is not in the VPN service's own docs, but it works (verified 2026-09-04 — returns `204` and the
87
+ tunnel disappears from the list):
88
+
89
+ ```bash
90
+ curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" \
91
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels/$ID
92
+ ```
93
+
94
+ That invalidates the tunnel server-side, so it is the real off-switch for VPN access when someone leaves —
95
+ do it *and* revoke their kubectl permissions (§3).
96
+
97
+ ---
98
+
99
+ ## 2. kubectl — create their credential
100
+
101
+ The cluster authenticates users with **x509 client certificates** signed by the cluster CA. The username is
102
+ the certificate's **Common Name (CN)**; RBAC is bound to that string.
103
+
104
+ You do all of it and hand over a finished `kubeconfig`.
105
+
106
+ > Everything below was verified end to end against the live cluster (2026-09-04) with a throwaway
107
+ > `guidetest-admin`: the resulting kubeconfig read `dooer-production` and `dooer-staging`, including
108
+ > secrets. The test user was then removed.
109
+
110
+ ```bash
111
+ USER_NAME=<firstname>-admin # becomes their cluster username
112
+ WORKDIR=$(mktemp -d) && cd "$WORKDIR"
113
+ ```
114
+
115
+ Work in a temp directory — it holds their private key until you hand it over, and you delete it at the end.
116
+
117
+ ### 2a. Generate their key and CSR
118
+
119
+ ```bash
120
+ openssl genrsa -out "$USER_NAME.key" 2048
121
+ openssl req -new -key "$USER_NAME.key" -out "$USER_NAME.csr" -subj "/CN=$USER_NAME"
122
+ ```
123
+
124
+ ### 2b. Submit it as a CertificateSigningRequest
125
+
126
+ ```bash
127
+ cat <<EOF | kubectl apply -f -
128
+ apiVersion: certificates.k8s.io/v1
129
+ kind: CertificateSigningRequest
130
+ metadata:
131
+ name: $USER_NAME
132
+ spec:
133
+ request: $(base64 -i "$USER_NAME.csr" | tr -d '\n')
134
+ signerName: kubernetes.io/kube-apiserver-client
135
+ expirationSeconds: 31536000 # 1 year
136
+ usages:
137
+ - client auth
138
+ EOF
139
+ ```
140
+
141
+ If the name already exists (a renewal), delete the old object first:
142
+ `kubectl delete csr "$USER_NAME" --ignore-not-found`.
143
+
144
+ ### 2c. Approve and extract the certificate
145
+
146
+ ```bash
147
+ kubectl certificate approve "$USER_NAME"
148
+
149
+ kubectl get csr "$USER_NAME" -o jsonpath='{.status.certificate}' | base64 -d > "$USER_NAME.crt"
150
+ openssl x509 -in "$USER_NAME.crt" -noout -subject -issuer -dates
151
+ ```
152
+
153
+ Expect `subject=CN=<firstname>-admin`, `issuer=CN=kubernetes`, and a one-year window.
154
+
155
+ If `.status.certificate` is empty the signer has not run yet — wait a couple of seconds and repeat. If it
156
+ stays empty, check `kubectl describe csr "$USER_NAME"`.
157
+
158
+ ### 2d. Extract the cluster CA
159
+
160
+ ```bash
161
+ kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt
162
+ ```
163
+
164
+ ### 2e. Bind permissions
165
+
166
+ This is what makes the certificate useful — without it they authenticate but can do nothing. These are the
167
+ same five ClusterRoles the existing admins (`jimmy-admin`, `sam-admin`, `trajko-admin`) hold:
168
+
169
+ ```bash
170
+ for r in admin gateway-creator namespace-service-role clusterissuer-admin network-admin; do
171
+ kubectl create clusterrolebinding "${r}-user-${USER_NAME}" --clusterrole="$r" --user="$USER_NAME"
172
+ done
173
+ ```
174
+
175
+ | ClusterRole | Grants |
176
+ | --- | --- |
177
+ | `admin` | full read/write inside namespaces (the built-in Kubernetes role) |
178
+ | `gateway-creator` | Gateway API resources |
179
+ | `namespace-service-role` | namespace-scoped service management |
180
+ | `clusterissuer-admin` | cert-manager ClusterIssuers (cluster-scoped) |
181
+ | `network-admin` | NetworkAttachmentDefinitions (Multus), cluster-wide |
182
+
183
+ The declarative equivalent lives in
184
+ `rosopr7102-roboten-hosting-services/clusterrole-admin.yaml`. Creating the bindings with `kubectl` as above
185
+ is enough for the cluster, but that file is the record of who has access — **add the new user there and
186
+ commit it**, or the next person reading it will get a false picture.
187
+
188
+ On a renewal, skip this step: the bindings are attached to the username, not to the certificate.
189
+
190
+ ### 2f. Assemble the kubeconfig
191
+
192
+ `--embed-certs=true` inlines the key and certificates, so the file you hand over is self-contained:
193
+
194
+ ```bash
195
+ SERVER=https://k8s.roboten-infra.com:64430
196
+ KC="$WORKDIR/$USER_NAME.kubeconfig"
197
+
198
+ KUBECONFIG="$KC" kubectl config set-cluster kubernetes \
199
+ --server="$SERVER" --certificate-authority=ca.crt --embed-certs=true
200
+
201
+ KUBECONFIG="$KC" kubectl config set-credentials "$USER_NAME" \
202
+ --client-certificate="$USER_NAME.crt" --client-key="$USER_NAME.key" --embed-certs=true
203
+
204
+ KUBECONFIG="$KC" kubectl config set-context "$USER_NAME@kubernetes" \
205
+ --cluster=kubernetes --user="$USER_NAME"
206
+
207
+ KUBECONFIG="$KC" kubectl config use-context "$USER_NAME@kubernetes"
208
+ ```
209
+
210
+ ### 2g. Verify it before handing it over
211
+
212
+ Test the actual file, not just the RBAC — with your own VPN up:
213
+
214
+ ```bash
215
+ KUBECONFIG="$KC" kubectl auth whoami # Username: <firstname>-admin
216
+ KUBECONFIG="$KC" kubectl get pods -n dooer-staging | head -3
217
+ KUBECONFIG="$KC" kubectl get pods -n dooer-production | head -3
218
+ KUBECONFIG="$KC" kubectl auth can-i get secrets -n dooer-staging # yes
219
+ ```
220
+
221
+ If `whoami` works but the reads say `Forbidden`, step 2e did not take.
222
+
223
+ ### 2h. Hand it over, then clean up
224
+
225
+ > **The kubeconfig contains their private key.** Send it the same way as the VPN config — a shared vault
226
+ > entry or an expiring secure link, never email, Slack or a ticket.
227
+
228
+ Tell them the context name (`<firstname>-admin@kubernetes`) and point them at
229
+ [getting-access.md § 2](./getting-access.md#2-kubectl-access).
230
+
231
+ Once they confirm it works, destroy your copy — you are holding their key until you do:
232
+
233
+ ```bash
234
+ rm -rf "$WORKDIR"
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 3. Revoking access
240
+
241
+ ```bash
242
+ for r in admin gateway-creator namespace-service-role clusterissuer-admin network-admin; do
243
+ kubectl delete clusterrolebinding "${r}-user-${USER_NAME}"
244
+ done
245
+ kubectl delete csr "$USER_NAME" # tidiness only; the CSR object is just a record
246
+ ```
247
+
248
+ Also remove them from `clusterrole-admin.yaml` and commit.
249
+
250
+ > **A signed certificate cannot be revoked.** Kubernetes has no CRL or OCSP: until it expires, that
251
+ > certificate still *authenticates* — deleting the bindings only removes what it is *allowed to do*, which
252
+ > leaves the holder as an authenticated user with no permissions. That is the real control, and it is why
253
+ > certificates are issued for one year rather than indefinitely. If a key is actually compromised, removing
254
+ > the bindings is the immediate mitigation; rotating the cluster CA is the only complete one, and that
255
+ > invalidates **everyone's** certificate at once — do not do it without planning the re-issue for all users.
256
+
257
+ ---
258
+
259
+ ## Renewals
260
+
261
+ A certificate expiring is routine. Repeat **§2a–2d and 2f–2h** and send them a fresh kubeconfig; they
262
+ re-run [getting-access.md § 2](./getting-access.md#2-kubectl-access).
263
+
264
+ Skip **2e** — ClusterRoleBindings are bound to the username, not to the certificate, so they survive a
265
+ re-issue untouched.
266
+
267
+ Delete the old CSR object first, or the new one collides on the name:
268
+
269
+ ```bash
270
+ kubectl delete csr "$USER_NAME" --ignore-not-found
271
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooer/dooer-test-env",
3
- "version": "1.11.2",
3
+ "version": "1.13.0",
4
4
  "description": "Run the whole Dooer backend locally (staging DB minus customers), copy/purge customers between environments, and shred — one CLI.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": "Dooer/cli-dooer-test-env",
@@ -13,6 +13,7 @@
13
13
  "files": [
14
14
  "bin/",
15
15
  "lib/",
16
+ "docs/",
16
17
  "local-postgres/",
17
18
  "discovery-router/"
18
19
  ],
package/readme.md CHANGED
@@ -12,11 +12,23 @@ See **[ENVIRONMENT-PLAN.md](./ENVIRONMENT-PLAN.md)** for the full design and rat
12
12
  npx @dooer/dooer-test-env <command>
13
13
  ```
14
14
 
15
+ ## First time here?
16
+
17
+ `setup` needs **VPN access** and a **kubectl credential** for the cluster — the compose file, the registry
18
+ logins and the BankID cert all come from there.
19
+
20
+ - **[Getting access](./docs/getting-access.md)** — new developer: set up the VPN, get your kubectl
21
+ certificate, and verify both actually work.
22
+ - **[Issuing access](./docs/issuing-access.md)** — admin: sign someone's certificate, bind their
23
+ permissions, and revoke them again.
24
+
25
+ Already have both? Straight to the quick start.
26
+
15
27
  ## Quick start
16
28
 
17
29
  ```bash
18
30
  # 1. One-time: prereq checks, registry login, compose generation, BankID cert → keychain.
19
- # Needs the VPN + kubectl access to dooer-staging.
31
+ # Needs the VPN + kubectl access to dooer-staging (see "First time here?" above).
20
32
  npx @dooer/dooer-test-env setup
21
33
 
22
34
  # 2. Bring the whole stack up (all staging services + frontends)
@@ -272,3 +284,5 @@ Inbound *input* validation always runs — this only affects response validation
272
284
  ## Prerequisites
273
285
 
274
286
  Docker (Desktop), `kubectl` with access to the `dooer-staging` namespace (VPN), and Node 18+.
287
+
288
+ Don't have the VPN or a kubectl credential yet? → **[Getting access](./docs/getting-access.md)**.