@dooer/dooer-test-env 1.11.2 → 1.12.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,239 @@
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
+ The VPN part is self-service. The kubectl part needs an admin to sign your certificate — see
10
+ [issuing-access.md](./issuing-access.md) for their side.
11
+
12
+ ---
13
+
14
+ ## 0. Prerequisites
15
+
16
+ ```bash
17
+ brew install wireguard-tools go jq kubernetes-cli
18
+ ```
19
+
20
+ `openssl` ships with macOS. Check everything is there:
21
+
22
+ ```bash
23
+ wg --version && go version && jq --version && kubectl version --client && openssl version
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 1. VPN
29
+
30
+ ### 1a. Get a Robo10 token
31
+
32
+ The VPN API is authenticated with a Robo10 access token. This opens a browser — **log in with your
33
+ `@robo10.com` email**.
34
+
35
+ ```bash
36
+ TOKEN="$(go run github.com/strehle/cmdline-openid-client/openid-client@latest \
37
+ -issuer https://oidc-provider-backend.api.staging.roboten-dev.com \
38
+ -client_id ro_authentication_cl_id \
39
+ -port 34466 | awk '/===/ { if (start) nextfile; start = 1; next } start { print }' | jq -r .access_token)"
40
+
41
+ [ -n "$TOKEN" ] && echo "token acquired" || echo "no token — did the browser login complete?"
42
+ ```
43
+
44
+ Keep using **this same terminal** for the next steps: `$TOKEN` only exists in this shell.
45
+
46
+ <details>
47
+ <summary>Alternative if the Go client will not run</summary>
48
+
49
+ 1. Open <https://chat.roboten.com> and log in.
50
+ 2. DevTools → **Application** → **Storage → Cookies**.
51
+ 3. Copy the value of the `x-op-token` cookie.
52
+ 4. In your terminal: `TOKEN=<the-cookie-value>`
53
+ </details>
54
+
55
+ ### 1b. Create your tunnel
56
+
57
+ One tunnel per person per machine. Name it after yourself so admins can tell them apart:
58
+
59
+ ```bash
60
+ NAME="$(whoami)-$(hostname -s)"
61
+ curl -sS -H "Authorization: Bearer $TOKEN" \
62
+ -H 'Content-Type: application/json' \
63
+ -d "{ \"name\": \"$NAME\" }" \
64
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels
65
+ ```
66
+
67
+ The response contains the tunnel's **id** — you need it next. To find it again later:
68
+
69
+ ```bash
70
+ curl -sS -H "Authorization: Bearer $TOKEN" https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels
71
+ ```
72
+
73
+ ### 1c. Download the tunnel config
74
+
75
+ ```bash
76
+ ID=<the id from the previous step>
77
+ curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
78
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels/$ID/actions/get-config \
79
+ > ~/roboten.conf
80
+ ```
81
+
82
+ > This file contains your **private key**. Treat it like a password: never commit it, never paste it into
83
+ > chat or a ticket.
84
+
85
+ ### 1d. Install it
86
+
87
+ `wg-quick` takes the interface name from the **filename**, so the name you choose here is the name you use
88
+ in every later command. Pick something that will not collide with a tunnel you already have:
89
+
90
+ ```bash
91
+ sudo mkdir -p /opt/homebrew/etc/wireguard
92
+ sudo cp ~/roboten.conf /opt/homebrew/etc/wireguard/roboten.conf
93
+ sudo chmod 600 /opt/homebrew/etc/wireguard/roboten.conf
94
+ rm ~/roboten.conf # the copy under /opt/homebrew is the one wg-quick reads
95
+ ```
96
+
97
+ Check first whether that name is already taken — **do not overwrite someone else's tunnel**:
98
+
99
+ ```bash
100
+ ls /opt/homebrew/etc/wireguard/
101
+ ```
102
+
103
+ ### 1e. Connect, disconnect, restart
104
+
105
+ ```bash
106
+ sudo wg-quick up roboten # connect
107
+ sudo wg-quick down roboten # disconnect
108
+ sudo wg-quick down roboten && sudo wg-quick up roboten # restart (after a config change or a sleep/wake)
109
+
110
+ sudo wg show # status: handshake time + transfer counters
111
+ ```
112
+
113
+ `wg-quick` is not a daemon: the tunnel does **not** survive a reboot, and after a laptop sleep it sometimes
114
+ needs the restart above. If `wg show` reports a `latest handshake` older than a couple of minutes while you
115
+ are actively using the cluster, restart it.
116
+
117
+ To start it automatically at boot:
118
+
119
+ ```bash
120
+ sudo brew services start wireguard-tools # starts wg0 by default
121
+ ```
122
+
123
+ That service is hard-wired to the `wg0` interface, so it only helps if you named your file `wg0.conf`.
124
+
125
+ ### 1f. Verify the VPN works
126
+
127
+ ```bash
128
+ sudo wg show # expect a recent "latest handshake"
129
+ nc -vz k8s.roboten-infra.com 64430 # expect: succeeded / open
130
+ ```
131
+
132
+ If `nc` hangs or is refused, the tunnel is not carrying traffic — restart it (1e). Until this passes, no
133
+ amount of kubectl configuration will help.
134
+
135
+ ---
136
+
137
+ ## 2. kubectl access
138
+
139
+ Authentication is a **client certificate**. You generate the key and the signing request; only the request
140
+ leaves your machine. Your private key never does.
141
+
142
+ ### 2a. Generate your key and CSR
143
+
144
+ Use `<firstname>-admin` as the name — that string becomes your username in the cluster, and it is what the
145
+ admin binds permissions to.
146
+
147
+ ```bash
148
+ USER_NAME=<firstname>-admin
149
+ mkdir -p ~/.kube/roboten && cd ~/.kube/roboten
150
+
151
+ openssl genrsa -out "$USER_NAME.key" 2048
152
+ openssl req -new -key "$USER_NAME.key" -out "$USER_NAME.csr" -subj "/CN=$USER_NAME"
153
+ chmod 600 "$USER_NAME.key"
154
+ ```
155
+
156
+ ### 2b. Send the CSR to an admin
157
+
158
+ Send **only** `<firstname>-admin.csr`. It is not secret — but the `.key` next to it is, and must never
159
+ leave your machine.
160
+
161
+ Ask them for three things back:
162
+
163
+ | | |
164
+ | --- | --- |
165
+ | `<firstname>-admin.crt` | your signed certificate |
166
+ | `ca.crt` | the cluster CA certificate |
167
+ | the API server URL | currently `https://k8s.roboten-infra.com:64430` |
168
+
169
+ ### 2c. Assemble your kubeconfig
170
+
171
+ Put the two `.crt` files next to your key in `~/.kube/roboten`, then:
172
+
173
+ ```bash
174
+ cd ~/.kube/roboten
175
+ USER_NAME=<firstname>-admin
176
+ SERVER=https://k8s.roboten-infra.com:64430
177
+
178
+ KUBECONFIG=~/.kube/config kubectl config set-cluster kubernetes \
179
+ --server="$SERVER" --certificate-authority=ca.crt --embed-certs=true
180
+
181
+ KUBECONFIG=~/.kube/config kubectl config set-credentials "$USER_NAME" \
182
+ --client-certificate="$USER_NAME.crt" --client-key="$USER_NAME.key" --embed-certs=true
183
+
184
+ KUBECONFIG=~/.kube/config kubectl config set-context "$USER_NAME@kubernetes" \
185
+ --cluster=kubernetes --user="$USER_NAME"
186
+
187
+ KUBECONFIG=~/.kube/config kubectl config use-context "$USER_NAME@kubernetes"
188
+ ```
189
+
190
+ `--embed-certs=true` copies the certificates *into* `~/.kube/config`, so the files in `~/.kube/roboten` are
191
+ only a backup after this point. Keep the `.key` anyway — you need it if you ever rebuild the config.
192
+
193
+ ### 2d. Verify
194
+
195
+ With the VPN up:
196
+
197
+ ```bash
198
+ kubectl auth whoami
199
+ # Username <firstname>-admin
200
+ # Groups [system:authenticated]
201
+
202
+ kubectl get pods -n dooer-staging | head -3
203
+ kubectl get pods -n dooer-production | head -3
204
+ ```
205
+
206
+ Then the two checks `dooer-test-env` actually depends on:
207
+
208
+ ```bash
209
+ kubectl auth can-i get secrets -n dooer-staging # yes — registry pull secrets + BankID cert
210
+ kubectl get svc dooer-database -n dooer-staging # the database service the base-DB build reads
211
+ ```
212
+
213
+ All of those must succeed before `dooer-test-env setup` will work.
214
+
215
+ ---
216
+
217
+ ## 3. You are done
218
+
219
+ ```bash
220
+ npx @dooer/dooer-test-env@latest setup
221
+ ```
222
+
223
+ `setup` re-checks all of the above and tells you exactly which piece is missing if something is wrong.
224
+
225
+ ---
226
+
227
+ ## Troubleshooting
228
+
229
+ | Symptom | Cause |
230
+ | --- | --- |
231
+ | `dial tcp … i/o timeout` from kubectl | VPN is down — `sudo wg show`, then restart it (1e) |
232
+ | `nc` to the API server hangs | Tunnel is up but not carrying traffic; restart it |
233
+ | `error: You must be logged in to the server (Unauthorized)` | Certificate is wrong/expired, or the cluster does not know that CN — back to 2b |
234
+ | `Error from server (Forbidden)` | You *are* authenticated; your user has no permissions bound yet — ask the admin to complete their step 3 |
235
+ | `no token — did the browser login complete?` | The OIDC flow did not finish; make sure you used your `@robo10.com` account |
236
+ | `wg-quick: 'roboten' already exists` | The interface is already up — `sudo wg-quick down roboten` first |
237
+
238
+ Certificates are issued for **one year**. When yours expires you repeat section 2 — the admin only has to
239
+ re-sign; your permissions (step 3 on their side) stay in place.
@@ -0,0 +1,180 @@
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** | self-service | make sure they have a Robo10 login; nothing to hand out |
8
+ | **kubectl credential** | needs you | sign their CSR and bind their permissions |
9
+
10
+ You need `kubectl` with cluster-admin (`system:masters`) and the VPN up.
11
+
12
+ ---
13
+
14
+ ## 1. VPN — what you actually have to do
15
+
16
+ Tunnels are **self-service**: any valid Robo10 token can create one, and the config is generated on demand
17
+ by the VPN API. So there is no artefact for you to produce or send.
18
+
19
+ Your only job is to make sure the person can log in at <https://chat.roboten.com> with their
20
+ **`@robo10.com`** account — that same identity is what the token flow authenticates. If they cannot log in,
21
+ that is an account-provisioning issue in Robo10, not something this guide covers.
22
+
23
+ Then point them at [getting-access.md § 1](./getting-access.md#1-vpn).
24
+
25
+ **Never** create the tunnel on their behalf and send them the config: the config embeds a private key, so a
26
+ tunnel they generated themselves is the only one only they hold.
27
+
28
+ To see who has tunnels (with your own token, as in the dev guide):
29
+
30
+ ```bash
31
+ curl -sS -H "Authorization: Bearer $TOKEN" https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels
32
+ ```
33
+
34
+ To revoke one — `DELETE` is not in the VPN service's own docs, but it works (verified 2026-09-04, returns
35
+ `204` and the tunnel disappears from the list):
36
+
37
+ ```bash
38
+ curl -sS -X DELETE -H "Authorization: Bearer $TOKEN" \
39
+ https://vpn.api.staging.roboten-dev.com/v1/vpn-tunnels/$ID
40
+ ```
41
+
42
+ That invalidates the tunnel's key server-side, so it is the real off-switch for VPN access when someone
43
+ leaves — do it *and* revoke their kubectl permissions (§3).
44
+
45
+ ---
46
+
47
+ ## 2. kubectl — sign their certificate
48
+
49
+ The cluster authenticates users with **x509 client certificates** signed by the cluster CA. The username is
50
+ the certificate's **Common Name (CN)**; RBAC is bound to that string.
51
+
52
+ You never see their private key. They send you a `.csr`; you send back a `.crt`.
53
+
54
+ ### 2a. Submit their CSR to the cluster
55
+
56
+ ```bash
57
+ USER_NAME=<firstname>-admin # must match the CN in their CSR
58
+ CSR_FILE=~/Downloads/$USER_NAME.csr # what they sent you
59
+
60
+ cat <<EOF | kubectl apply -f -
61
+ apiVersion: certificates.k8s.io/v1
62
+ kind: CertificateSigningRequest
63
+ metadata:
64
+ name: $USER_NAME
65
+ spec:
66
+ request: $(base64 -i "$CSR_FILE" | tr -d '\n')
67
+ signerName: kubernetes.io/kube-apiserver-client
68
+ expirationSeconds: 31536000 # 1 year
69
+ usages:
70
+ - client auth
71
+ EOF
72
+ ```
73
+
74
+ Sanity-check the CN is what you expect before approving — the CN is the identity you are about to grant
75
+ admin to:
76
+
77
+ ```bash
78
+ openssl req -in "$CSR_FILE" -noout -subject
79
+ # subject=CN=<firstname>-admin
80
+ ```
81
+
82
+ ### 2b. Approve and extract the certificate
83
+
84
+ ```bash
85
+ kubectl certificate approve "$USER_NAME"
86
+
87
+ kubectl get csr "$USER_NAME" -o jsonpath='{.status.certificate}' | base64 -d > "$USER_NAME.crt"
88
+ openssl x509 -in "$USER_NAME.crt" -noout -subject -issuer -dates
89
+ ```
90
+
91
+ Expect `subject=CN=<firstname>-admin`, `issuer=CN=kubernetes`, and a one-year window.
92
+
93
+ If `.status.certificate` is empty, the signer has not run yet — wait a couple of seconds and repeat. If it
94
+ stays empty, the CSR was approved but not signed; check `kubectl describe csr "$USER_NAME"`.
95
+
96
+ ### 2c. Extract the cluster CA
97
+
98
+ The same file for everyone:
99
+
100
+ ```bash
101
+ kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt
102
+ ```
103
+
104
+ ### 2d. Bind permissions
105
+
106
+ This is what makes the certificate useful — without it they authenticate but can do nothing. These are the
107
+ same five ClusterRoles the existing admins (`jimmy-admin`, `sam-admin`, `trajko-admin`) hold:
108
+
109
+ ```bash
110
+ for r in admin gateway-creator namespace-service-role clusterissuer-admin network-admin; do
111
+ kubectl create clusterrolebinding "${r}-user-${USER_NAME}" --clusterrole="$r" --user="$USER_NAME"
112
+ done
113
+ ```
114
+
115
+ | ClusterRole | Grants |
116
+ | --- | --- |
117
+ | `admin` | full read/write inside namespaces (the built-in Kubernetes role) |
118
+ | `gateway-creator` | Gateway API resources |
119
+ | `namespace-service-role` | namespace-scoped service management |
120
+ | `clusterissuer-admin` | cert-manager ClusterIssuers (cluster-scoped) |
121
+ | `network-admin` | NetworkAttachmentDefinitions (Multus), cluster-wide |
122
+
123
+ The declarative equivalent lives in
124
+ `rosopr7102-roboten-hosting-services/clusterrole-admin.yaml`. Creating the bindings with `kubectl` as above
125
+ is enough for the cluster, but that file is the record of who has access — **add the new user there and
126
+ commit it**, or the next person reading it will get a false picture.
127
+
128
+ ### 2e. Hand back
129
+
130
+ Send them three things (none are secret — the secret half never left their machine):
131
+
132
+ 1. `<firstname>-admin.crt`
133
+ 2. `ca.crt`
134
+ 3. the API server URL — currently `https://k8s.roboten-infra.com:64430`
135
+
136
+ Then they follow [getting-access.md § 2c](./getting-access.md#2c-assemble-your-kubeconfig).
137
+
138
+ ### 2f. Verify before you tell them it is done
139
+
140
+ ```bash
141
+ kubectl get clusterrolebinding | grep "$USER_NAME" # expect 5 rows
142
+ kubectl auth can-i get pods -n dooer-production --as "$USER_NAME"
143
+ kubectl auth can-i get secrets -n dooer-staging --as "$USER_NAME"
144
+ ```
145
+
146
+ `--as` impersonates the user, so you are testing the real RBAC without needing their certificate.
147
+
148
+ ---
149
+
150
+ ## 3. Revoking access
151
+
152
+ ```bash
153
+ for r in admin gateway-creator namespace-service-role clusterissuer-admin network-admin; do
154
+ kubectl delete clusterrolebinding "${r}-user-${USER_NAME}"
155
+ done
156
+ kubectl delete csr "$USER_NAME" # tidiness only; the CSR object is just a record
157
+ ```
158
+
159
+ Also remove them from `clusterrole-admin.yaml` and commit.
160
+
161
+ > **A signed certificate cannot be revoked.** Kubernetes has no CRL or OCSP: until it expires, that
162
+ > certificate still *authenticates* — deleting the bindings only removes what it is *allowed to do*, which
163
+ > leaves the holder as an authenticated user with no permissions. That is the real control, and it is why
164
+ > certificates are issued for one year rather than indefinitely. If a key is actually compromised, removing
165
+ > the bindings is the immediate mitigation; rotating the cluster CA is the only complete one, and that
166
+ > invalidates **everyone's** certificate at once — do not do it without planning the re-issue for all users.
167
+
168
+ ---
169
+
170
+ ## Renewals
171
+
172
+ A certificate expiring is routine: the user repeats [getting-access.md § 2a–2c](./getting-access.md#2a-generate-your-key-and-csr)
173
+ and you repeat **2a–2c and 2e** here. Skip 2d — their ClusterRoleBindings are bound to the username, not to
174
+ the certificate, so they survive a re-issue untouched.
175
+
176
+ Delete the old CSR object first, or the new one collides on the name:
177
+
178
+ ```bash
179
+ kubectl delete csr "$USER_NAME" --ignore-not-found
180
+ ```
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.12.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)**.