From 57e8930dddaaa637802756675eed9fbc98e3aadd Mon Sep 17 00:00:00 2001 From: natnesi <111834566+natnesi@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:50:17 +0000 Subject: [PATCH 1/2] discussion-ssh-security --- docs/cards-home.yaml | 2 +- docs/user-guides/.pages.yml | 1 + ...anaging-application-credentials-via-cli.md | 2 +- ...lication-credentials-with-the-dashboard.md | 4 +- .../create-and-manage-identity/index.md | 2 +- .../create-and-manage-keypairs-via-cli.md | 2 +- ...-and-manage-keypairs-with-the-dashboard.md | 2 +- .../create-and-manage-keypairs/index.md | 4 +- .../secure-and-rotate-ssh-keys.md | 471 ++++++++++++++++++ .../create-and-manage-networks/index.md | 2 +- ...create-and-manage-network-ports-via-cli.md | 2 +- .../create-and-manage-networks-via-cli.md | 2 +- .../manage-security-groups-via-cli.md | 2 +- ...manage-network-ports-with-the-dashboard.md | 2 +- ...-and-manage-networks-with-the-dashboard.md | 2 +- ...nage-security-groups-with-the-dashboard.md | 2 +- ...ng-and-managing-ec2-credentials-via-cli.md | 2 +- ...reate-and-manage-object-storage-via-cli.md | 2 +- ...anage-object-storage-with-the-dashboard.md | 2 +- .../format-and-mount-volumes.md | 2 +- .../create-and-manage-volumes/index.md | 4 +- .../create-and-manage-volumes-via-cli.md | 2 +- ...e-and-manage-volumes-with-the-dashboard.md | 2 +- docs/user-guides/index.md | 6 + .../default-user-nesi-images.md | 2 +- .../launch-and-manage-instances/index.md | 2 +- .../launch-an-instance-from-an-image.md | 2 +- ...urity-groups-of-an-Instance-via-the-cli.md | 2 +- .../launch-an-instance-via-dashboard.md | 2 +- ...groups-of-an-Instance-via-the-dashboard.md | 2 +- .../resizing-an-Instance-via-the-dashboard.md | 2 +- .../upload-and-manage-images/index.md | 2 +- .../with-the-cli/managing-images-via-cli.md | 2 +- .../with-the-cli/upload-an-image-via-cli.md | 2 +- .../managing-images-via-the-dashboard.md | 2 +- .../upload-an-image-via-the-dashboard.md | 2 +- .../use-a-service-account/.pages.yml | 1 + .../use-a-service-account/index.md | 172 +++++++ 38 files changed, 688 insertions(+), 35 deletions(-) create mode 100644 docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md create mode 100644 docs/user-guides/use-a-service-account/.pages.yml create mode 100644 docs/user-guides/use-a-service-account/index.md diff --git a/docs/cards-home.yaml b/docs/cards-home.yaml index 8bde310..d11626c 100644 --- a/docs/cards-home.yaml +++ b/docs/cards-home.yaml @@ -6,4 +6,4 @@ - title: Talk to us content: | If you want to chat with us about how our platform can support your research - url: mailto:support@cloud.nesi.org.nz?subject=NeSI's%20Research%20Developer%20Cloud \ No newline at end of file + url: mailto:support@cloud.nesi.org.nz?subject=Research%20Developer%20Cloud diff --git a/docs/user-guides/.pages.yml b/docs/user-guides/.pages.yml index a642078..6d9aed8 100644 --- a/docs/user-guides/.pages.yml +++ b/docs/user-guides/.pages.yml @@ -4,6 +4,7 @@ nav: - upload-and-manage-images - create-and-manage-volumes - create-and-manage-networks + - use-a-service-account - create-and-manage-identity - create-and-manage-keypairs - create-and-manage-object-storage diff --git a/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-via-cli.md b/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-via-cli.md index 4eb9361..c312665 100644 --- a/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-via-cli.md +++ b/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-via-cli.md @@ -5,7 +5,7 @@ label_names: - create - manage - cli -title: Creating and Managing Application Credentials via CLI +title: Create and Manage Application Credentials via CLI --- !!! note diff --git a/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-with-the-dashboard.md b/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-with-the-dashboard.md index 6e3661f..0a9ebed 100644 --- a/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-identity/creating-and-managing-application-credentials-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage networks via the dashboard +title: Create and Manage Networks via the dashboard --- ## Create a Network @@ -98,4 +98,4 @@ Using the check box select the `Application Credentials` you would like to delet Click `Delete Application Credentials` and a confirmation dialog will appear -Confirm the deletion by clicking `Delete Application Credentials` \ No newline at end of file +Confirm the deletion by clicking `Delete Application Credentials` diff --git a/docs/user-guides/create-and-manage-identity/index.md b/docs/user-guides/create-and-manage-identity/index.md index b5b726d..40f0803 100644 --- a/docs/user-guides/create-and-manage-identity/index.md +++ b/docs/user-guides/create-and-manage-identity/index.md @@ -5,7 +5,7 @@ label_names: - create - manage position: 1 -title: Create and Manage identity +title: Create and Manage Identity vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-via-cli.md b/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-via-cli.md index ef7e510..03a2379 100644 --- a/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-via-cli.md +++ b/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-via-cli.md @@ -6,7 +6,7 @@ label_names: - manage - cli position: 1 -title: Create and manage keypairs via CLI +title: Create and Manage Keypairs via CLI --- !!! note diff --git a/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-with-the-dashboard.md b/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-with-the-dashboard.md index c4825ad..97d4434 100644 --- a/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-keypairs/create-and-manage-keypairs-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage keypairs via the dashboard +title: Create and Manage Keypairs via the dashboard --- ## Create a Keypair diff --git a/docs/user-guides/create-and-manage-keypairs/index.md b/docs/user-guides/create-and-manage-keypairs/index.md index 552d74c..34b1de8 100644 --- a/docs/user-guides/create-and-manage-keypairs/index.md +++ b/docs/user-guides/create-and-manage-keypairs/index.md @@ -19,4 +19,6 @@ Key pairs can be managed a few ways - [Create and manage key pairs via the dashboard](create-and-manage-keypairs-with-the-dashboard.md) -- [Create and manage key pairs via CLI](create-and-manage-keypairs-via-cli.md) \ No newline at end of file +- [Create and manage key pairs via CLI](create-and-manage-keypairs-via-cli.md) + +- [Secure and rotate ssh keys](secure-and-rotate-ssh-keys.md) diff --git a/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md b/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md new file mode 100644 index 0000000..a923f71 --- /dev/null +++ b/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md @@ -0,0 +1,471 @@ +--- +hidden: false +label_names: +- keypairs +- security +- ssh +position: 1 +title: Secure and Rotate SSH Keys +description: Protect an SSH private key with a passphrase, and rotate a key on the Research Developer Cloud without losing access to your running instances. +--- + +An SSH private key is the credential that stands between the public internet and +your instances. Unlike a password it never expires, nothing prompts you to +change it, and a forgotten copy can sit on an old laptop for years. Treat the +private key as you would treat a password: protect it, know where every copy +is, and replace it on a schedule. + +This page explains how to protect a key, then gives a step-by-step procedure +for rotating one. + +!!! warning + Deleting a keypair in the RDC does **not** revoke access to instances that + are already running. OpenStack uses the keypair record only to inject your + public key when an instance is **first built**. After that the key lives in + `~/.ssh/authorized_keys` on the instance and OpenStack no longer tracks it. + To retire a key you must remove it from `authorized_keys` on every instance + that has it. + +That single fact shapes the whole procedure below: an instance keeps trusting a +key until you go to the instance and remove it. + +## What each action affects + +| Action | Running instances | New instances | +| --- | --- | --- | +| Upload a new public key to the RDC | No effect | Get the new key | +| Delete a keypair in the RDC | No effect | Cannot use that name | +| Edit `authorized_keys` on a host | Takes effect at once | No effect | +| Rebuild an instance with a new key | That one instance gets the new key, and is reinstalled | No effect | + +## Good practice checklist + +| Practice | Why it matters | +| --- | --- | +| Generate keys on your own machine | A key generated by the server was known to the server and crossed the network | +| Set a passphrase on every private key | A stolen laptop or a stray backup is then useless to whoever holds it | +| Prefer `ed25519` | Short, fast, and no key-size trap. Use RSA 4096 only where something old refuses `ed25519` | +| One key per person, per device | A shared key cannot be revoked for one person, and logs cannot tell you who connected | +| Build instances with a service account | Access then survives someone leaving. See [Use a Service Account](../use-a-service-account/index.md) | +| Never send a private key anywhere | Not by email, chat, ticket or shared drive. Move the public key instead | +| Set `600` on the key and `700` on `~/.ssh` | OpenSSH refuses to use a private key that others can read | +| Give every key a comment | `name@laptop-2026-08` tells you whose key it is and when it was made | +| Use an agent, never `ssh -A` | Agent forwarding lets the remote host borrow your key. `ProxyJump` does not | +| Rotate on a schedule and after events | See [When to rotate a key](#when-to-rotate-a-key) | + +## Create a protected key + +!!! note + `openstack keypair create KEY_PAIR_NAME` asks the cloud to generate the + key for you and returns the private half over the network, with no + passphrase. Generate the key on your own machine instead, and upload only + the public half. See + [Create and manage keypairs via CLI](create-and-manage-keypairs-via-cli.md) + for the full command reference. + +Generate the pair on your workstation: + +``` { .sh } +ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_ed25519 +``` + +| Option | Meaning | +| --- | --- | +| `-t ed25519` | Key type. Fast, small, and secure at a fixed strength | +| `-a 100` | Rounds used to derive the encryption key from your passphrase, which slows down anyone guessing it | +| `-C` | A comment stored in the key, so you can identify it later | +| `-f` | Where to write the pair | + +You are prompted for a passphrase. Set one. Use a phrase of several unrelated +words that you do not use anywhere else, and store it in your password manager. +The passphrase encrypts the private key on disk, so a copied key file is +worthless without it. + +The command writes two files: + +| File | Contents | Who may see it | +| --- | --- | --- | +| `id_rdc_ed25519` | Private key, encrypted with your passphrase | You only | +| `id_rdc_ed25519.pub` | Public key | Anyone | + +Then fix the permissions: + +``` { .sh } +chmod 700 ~/.ssh +chmod 600 ~/.ssh/id_rdc_ed25519 +``` + +### Add a passphrase later + +To add or change the passphrase on a key you already hold, without changing +the key itself: + +``` { .sh } +ssh-keygen -p -f ~/.ssh/id_rdc_ed25519 +``` + +!!! warning + This protects the file from here on, but the key material does not change. + If the key may already have been copied or exposed, a new passphrase + achieves nothing. Generate a new key and rotate. + +### Check a key fingerprint + +A fingerprint identifies a key without revealing it, and lets you confirm that +the key in the RDC is the one on your laptop: + +``` { .sh } +ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub +``` + +``` { .sh .no-copy } +256 SHA256:tMVbtbRdT2vmaZU2ciC5LLWrbql5yw6X8L+OMujXJ1Y name@laptop-2026-08 (ED25519) +``` + +The RDC displays MD5 fingerprints, so add `-E md5` when comparing against +`openstack keypair list`: + +``` { .sh } +ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub -E md5 +``` + +``` { .sh .no-copy } +256 MD5:57:71:50:18:ac:c2:ed:d9:54:05:c0:62:42:68:1d:3e name@laptop-2026-08 (ED25519) +``` + +## Use an SSH agent + +A passphrase prompt on every connection is the main reason people leave keys +unprotected. An agent solves that: unlock the key once per session, and the +agent holds it in memory. + +Start an agent and add the key: + +``` { .sh } +eval "$(ssh-agent -s)" +ssh-add -t 8h ~/.ssh/id_rdc_ed25519 +``` + +`-t 8h` makes the agent forget the key after eight hours, so an unattended +laptop does not stay unlocked overnight. List what the agent currently holds +with `ssh-add -l`, and drop everything with `ssh-add -D`. + +Add an entry to `~/.ssh/config` so the right key is offered automatically: + +``` { .sh } +Host rdc-* + User ubuntu + IdentityFile ~/.ssh/id_rdc_ed25519 + IdentitiesOnly yes + AddKeysToAgent yes +``` + +| Platform | Notes | +| --- | --- | +| Linux | Most desktops start an agent at login, so `eval` is rarely needed | +| macOS | Use `ssh-add --apple-use-keychain` and add `UseKeychain yes` to the config | +| Windows | Run `Set-Service ssh-agent -StartupType Automatic`, then `Start-Service ssh-agent`, in an elevated PowerShell | + +!!! warning + Do not use agent forwarding (`ssh -A` or `ForwardAgent yes`). Anyone with + root on the machine you connect to can use your forwarded agent to + authenticate as you elsewhere. Use `ProxyJump` instead, which keeps the key + on your workstation: + + ``` { .sh } + ssh -J ubuntu@BASTION_IP ubuntu@NODE_IP + ``` + +## When to rotate a key + +Rotate on a schedule, and immediately on any of the trigger events. + +| Trigger | Urgency | +| --- | --- | +| Routine cycle | Every 12 months | +| Someone leaves the team or project | Same day, see [When someone leaves](../create-and-manage-identity/use-a-service-account.md#when-someone-leaves) | +| A laptop or phone is lost or stolen | Immediately | +| A private key was sent over email or chat | Immediately | +| A key was stored without a passphrase on a shared machine | Immediately | +| An instance shows signs of compromise | Immediately, and see [If a key is compromised](#if-a-key-is-compromised) | +| A key was shared between people | At the next window, then stop sharing | + +## Rotate a key step by step + +The order matters. Add and prove the new key before you remove the old one, so +that a mistake never locks you out. + +!!! note + Before you start, confirm you have a second way in: console access through + the [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/), or a colleague + with their own working key. Recovering an instance whose only key is gone + is slow. + +### 1. Check what you have + +List the keypairs on your account and note the name, type and fingerprint of +the one you are replacing: + +``` { .sh } +openstack keypair list +``` + +``` { .sh .no-copy } ++-----------------+-------------------------------------------------+------+ +| Name | Fingerprint | Type | ++-----------------+-------------------------------------------------+------+ +| name-rdc-2025-03 | f0:4f:53:d1:3d:aa:71:d9:ef:b6:32:22:88:c2:68:42 | ssh | ++-----------------+-------------------------------------------------+------+ +``` + +Then list every instance that was built with it, so you know the full set of +hosts to update: + +``` { .sh } +openstack server list --long -c Name -c "Key Name" -c Networks +``` + +Add anything the RDC cannot see: nodes reached through a bastion, hosts you +added the key to by hand, and any automation that holds a copy. + +### 2. Generate the new key + +Follow [Create a protected key](#create-a-protected-key), giving the new key +its own filename and a comment that dates it: + +``` { .sh } +ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_2026_08 +``` + +Keep the old key in place for now. You need it to install the new one. + +### 3. Add the new key + +Append the new public key to `authorized_keys` on every host from step 1, +authenticating with the old key. The new key is added alongside the old, so +nothing breaks yet: + +``` { .sh } +NEWPUB=$(cat ~/.ssh/id_rdc_2026_08.pub) +ssh -i ~/.ssh/id_rdc_2025_03 ubuntu@INSTANCE_IP \ + "umask 077; mkdir -p ~/.ssh; grep -qxF '$NEWPUB' ~/.ssh/authorized_keys \ + || echo '$NEWPUB' >> ~/.ssh/authorized_keys" +``` + +The `grep -qxF` test makes the command safe to run twice: it appends the key +only if it is not already there. + +Repeat for every host. For more than a handful, see +[Rotate keys on a cluster](#rotate-keys-on-a-cluster). + +### 4. Test the new key + +Leave your current session open. In a **second terminal**, connect with the new +key only: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@INSTANCE_IP +``` + +`IdentitiesOnly=yes` stops SSH quietly falling back to the old key and giving +you a false pass. You should be prompted for the new passphrase, then let in. + +Do not continue until this succeeds on **every** host. + +### 5. Remove the old key + +Now revoke the old key where it actually matters. Match on the key body rather +than the comment, which anyone can edit: + +``` { .sh } +OLDPUB=$(awk '{print $2}' ~/.ssh/id_rdc_2025_03.pub) +ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP \ + "grep -v -F '$OLDPUB' ~/.ssh/authorized_keys > ~/.ssh/ak.new \ + && mv ~/.ssh/ak.new ~/.ssh/authorized_keys \ + && chmod 600 ~/.ssh/authorized_keys" +``` + +Confirm what is left, and that you recognise all of it: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP "ssh-keygen -lf ~/.ssh/authorized_keys" +``` + +Check other accounts on the host as well, `root` in particular: + +``` { .sh } +sudo ssh-keygen -lf /root/.ssh/authorized_keys +``` + +Delete the old private key from your workstation once every host is done. + +### 6. Update OpenStack + +Import the new public key so that instances built from now on receive it: + +``` { .sh } +openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub name-rdc-2026-08 +openstack keypair delete name-rdc-2025-03 +``` + +A dated name keeps the two keys visible side by side and makes the change +obvious in Terraform or Heat plans. + +!!! note + If the keypair name is pinned in a template you cannot easily edit, reuse + it instead. OpenStack keypairs cannot be updated in place, so delete and + re-create: + + ``` { .sh } + openstack keypair delete KEY_PAIR_NAME + openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub KEY_PAIR_NAME + ``` + + Between the two commands the name does not exist, so pause any automation + that builds instances. + +Confirm the fingerprint in the RDC matches the key on your workstation: + +``` { .sh } +openstack keypair list +ssh-keygen -lf ~/.ssh/id_rdc_2026_08.pub -E md5 +``` + +### 7. Record the change + +Note in your team documentation or ticket system: + +- The date, and who performed the rotation +- Old and new key names, and their fingerprints +- Which hosts were updated, and any that were skipped +- Why the key was rotated, if it was not the routine cycle +- The date the next routine rotation is due + +Fingerprints are the useful part: they let anyone confirm later which key a +host actually trusts. + +## Rotate keys on a cluster + +Kubernetes and other multi-node clusters need the same steps, applied to every +node. Choose one of the approaches below rather than editing nodes by hand. + +### Update every node + +Use configuration management so the result is repeatable and easy to audit. The +Ansible `authorized_key` module with `exclusive: true` sets the file to exactly +the keys you list, which handles the add and the remove in one pass: + +{% raw %} + +``` { .yaml } +- name: Set the authorised keys for the ubuntu user + ansible.posix.authorized_key: + user: ubuntu + state: present + exclusive: true + key: "{{ lookup('file', '~/.ssh/id_rdc_2026_08.pub') }}" +``` + +{% endraw %} + +!!! warning + `exclusive: true` removes every other key for that user, including any + added by a platform tool or a colleague. Run it against one node first, and + check the result before you run it against the rest. + +Reach nodes on a private network through the bastion: + +``` { .sh } +ansible-playbook -i inventory.ini rotate-keys.yml \ + --ssh-common-args '-o ProxyJump=ubuntu@BASTION_IP' +``` + +### Replace nodes instead + +Replacing nodes is usually cleaner than editing them. A node built after the +keypair is updated comes up with only the new key, so there is nothing to +remove and no chance of a missed host. + +Drain and replace one node at a time so the workload stays up: + +``` { .sh } +kubectl drain NODE_NAME --ignore-daemonsets --delete-emptydir-data +``` + +Delete the node through your cluster tooling, let the group build a +replacement, then confirm it before moving on: + +``` { .sh } +kubectl get nodes +``` + +!!! warning + `openstack server rebuild --key-name NEW_KEY SERVER` also resets the key, + but it rebuilds the instance from its image and destroys everything on the + root disk. Only use it where the instance holds no state. + +### Watch out for new nodes + +Editing `authorized_keys` on a running node changes that node alone. When the +cluster scales up, or replaces a failed node, the replacement is built from the +keypair recorded in the cluster or node-group definition. + +So update the definition as well as the nodes, or the old key reappears on the +next node that is built. After any scaling event, spot-check a new node: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@NEW_NODE_IP \ + "ssh-keygen -lf ~/.ssh/authorized_keys" +``` + +## If a key is compromised + +Assume the instances are compromised too, not only the key. Rotate straight +away rather than waiting for a maintenance window, and work through the checks +below on every host the key could reach. + +Look for keys you did not add, a common way of keeping access: + +``` { .sh } +ssh-keygen -lf ~/.ssh/authorized_keys +sudo ssh-keygen -lf /root/.ssh/authorized_keys +``` + +Review which keys have been used to log in, and from where: + +``` { .sh } +sudo journalctl -u ssh --grep "Accepted publickey" +``` + +Each line records the fingerprint that was accepted, so you can tell which +sessions were yours and which were not. On older images that still write a text +log, use `sudo grep "Accepted publickey" /var/log/auth.log` instead. + +Confirm the daemon is not accepting passwords as well: + +``` { .sh } +sudo sshd -T | grep -E "permitrootlogin|passwordauthentication" +``` + +`passwordauthentication` should read `no`. `permitrootlogin` reads +`prohibit-password` on the Ubuntu cloud images, which allows `root` in by key +only. Set it to `no` unless something genuinely needs `root` over SSH. + +Then work through the rest: + +- Rotate the key, following the procedure above +- Rotate any [application credentials](../create-and-manage-identity/index.md) + and EC2 credentials that the instance or the person held +- Rebuild rather than clean any instance you cannot fully account for +- Tighten the [security groups](../create-and-manage-networks/index.md) so that + port 22 is reachable only from the addresses you expect +- Please [contact Support](mailto:support@nesi.org.nz) if data may have been + exposed + +!!! note + An `authorized_keys` entry can be restricted to the addresses it will be + accepted from, which limits the value of a stolen key: + + ``` { .sh .no-copy } + from="10.0.0.0/24",restrict ssh-ed25519 AAAAC3Nz... name@laptop-2026-08 + ``` diff --git a/docs/user-guides/create-and-manage-networks/index.md b/docs/user-guides/create-and-manage-networks/index.md index 1506398..a22a84c 100644 --- a/docs/user-guides/create-and-manage-networks/index.md +++ b/docs/user-guides/create-and-manage-networks/index.md @@ -5,7 +5,7 @@ label_names: - create - manage position: 1 -title: Create and Manage networks +title: Create and Manage Networks vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-network-ports-via-cli.md b/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-network-ports-via-cli.md index c4678ec..576c691 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-network-ports-via-cli.md +++ b/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-network-ports-via-cli.md @@ -4,7 +4,7 @@ label_names: - instance - launch position: 2 -title: Create and manage network ports via CLI +title: Create and Manage Network Ports via CLI vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-networks-via-cli.md b/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-networks-via-cli.md index 126b808..91bbe29 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-networks-via-cli.md +++ b/docs/user-guides/create-and-manage-networks/with_the_CLI/create-and-manage-networks-via-cli.md @@ -4,7 +4,7 @@ label_names: - instance - launch position: 2 -title: Create and manage network via CLI +title: Create and Manage Network via CLI vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/create-and-manage-networks/with_the_CLI/manage-security-groups-via-cli.md b/docs/user-guides/create-and-manage-networks/with_the_CLI/manage-security-groups-via-cli.md index 340aae2..d9f3d44 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_CLI/manage-security-groups-via-cli.md +++ b/docs/user-guides/create-and-manage-networks/with_the_CLI/manage-security-groups-via-cli.md @@ -3,7 +3,7 @@ label_names: - security-groups - launch - cli -title: Manage Security groups via CLI +title: Manage Security Groups via CLI vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-network-ports-with-the-dashboard.md b/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-network-ports-with-the-dashboard.md index 7f4c4ae..e070787 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-network-ports-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-network-ports-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage network ports with the dashboard +title: Create and Manage Network Ports with the dashboard --- ## Create a Network port diff --git a/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-networks-with-the-dashboard.md b/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-networks-with-the-dashboard.md index fb76290..e8f23fb 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-networks-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-networks/with_the_dashboard/create-and-manage-networks-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage networks via the dashboard +title: Create and Manage Networks via the dashboard --- ## Create a Network diff --git a/docs/user-guides/create-and-manage-networks/with_the_dashboard/manage-security-groups-with-the-dashboard.md b/docs/user-guides/create-and-manage-networks/with_the_dashboard/manage-security-groups-with-the-dashboard.md index 28e99b3..7621175 100644 --- a/docs/user-guides/create-and-manage-networks/with_the_dashboard/manage-security-groups-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-networks/with_the_dashboard/manage-security-groups-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage Security groups with the dashboard +title: Create and Manage Security Groups with the dashboard --- ## The Default secuirty group diff --git a/docs/user-guides/create-and-manage-object-storage/creating-and-managing-ec2-credentials-via-cli.md b/docs/user-guides/create-and-manage-object-storage/creating-and-managing-ec2-credentials-via-cli.md index fe7a43d..c0f3e24 100644 --- a/docs/user-guides/create-and-manage-object-storage/creating-and-managing-ec2-credentials-via-cli.md +++ b/docs/user-guides/create-and-manage-object-storage/creating-and-managing-ec2-credentials-via-cli.md @@ -5,7 +5,7 @@ label_names: - create - manage - cli -title: Creating and Managing EC2 Credentials via CLI +title: Create and Manage EC2 Credentials via CLI --- ## Overview diff --git a/docs/user-guides/create-and-manage-object-storage/with_the_CLI/create-and-manage-object-storage-via-cli.md b/docs/user-guides/create-and-manage-object-storage/with_the_CLI/create-and-manage-object-storage-via-cli.md index 48621fa..42ef3b7 100644 --- a/docs/user-guides/create-and-manage-object-storage/with_the_CLI/create-and-manage-object-storage-via-cli.md +++ b/docs/user-guides/create-and-manage-object-storage/with_the_CLI/create-and-manage-object-storage-via-cli.md @@ -6,7 +6,7 @@ label_names: - manage - cli position: 1 -title: Create and manage object storage via CLI +title: Create and Manage Object Storage via CLI --- !!! note diff --git a/docs/user-guides/create-and-manage-object-storage/with_the_dashboard/create-and-manage-object-storage-with-the-dashboard.md b/docs/user-guides/create-and-manage-object-storage/with_the_dashboard/create-and-manage-object-storage-with-the-dashboard.md index f9b1c76..75e6ba5 100644 --- a/docs/user-guides/create-and-manage-object-storage/with_the_dashboard/create-and-manage-object-storage-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-object-storage/with_the_dashboard/create-and-manage-object-storage-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage object storage via the dashboard +title: Create and Manage Object Storage via the dashboard --- ## Creating an object storage container diff --git a/docs/user-guides/create-and-manage-volumes/format-and-mount-volumes.md b/docs/user-guides/create-and-manage-volumes/format-and-mount-volumes.md index 4d14a1b..9c9a341 100644 --- a/docs/user-guides/create-and-manage-volumes/format-and-mount-volumes.md +++ b/docs/user-guides/create-and-manage-volumes/format-and-mount-volumes.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Format and Mount volumes +title: Format and Mount Volumes --- # Format and Mount a Volume diff --git a/docs/user-guides/create-and-manage-volumes/index.md b/docs/user-guides/create-and-manage-volumes/index.md index 611b2a3..0cea8c7 100644 --- a/docs/user-guides/create-and-manage-volumes/index.md +++ b/docs/user-guides/create-and-manage-volumes/index.md @@ -5,7 +5,7 @@ label_names: - create - manage position: 1 -title: Create and manage volumes +title: Create and Manage Volumes vote_count: 1 vote_sum: 1 --- @@ -16,4 +16,4 @@ Volumes can be created a few ways - Create and manage volumes via the dashboard -- Create and manage volumes via CLI \ No newline at end of file +- Create and manage volumes via CLI diff --git a/docs/user-guides/create-and-manage-volumes/with_the_cli/create-and-manage-volumes-via-cli.md b/docs/user-guides/create-and-manage-volumes/with_the_cli/create-and-manage-volumes-via-cli.md index 9c0aabe..f4f0927 100644 --- a/docs/user-guides/create-and-manage-volumes/with_the_cli/create-and-manage-volumes-via-cli.md +++ b/docs/user-guides/create-and-manage-volumes/with_the_cli/create-and-manage-volumes-via-cli.md @@ -6,7 +6,7 @@ label_names: - manage - cli position: 1 -title: Create and manage volumes via CLI +title: Create and Manage Volumes via CLI --- !!! note diff --git a/docs/user-guides/create-and-manage-volumes/with_the_dashboard/create-and-manage-volumes-with-the-dashboard.md b/docs/user-guides/create-and-manage-volumes/with_the_dashboard/create-and-manage-volumes-with-the-dashboard.md index fa3e5bd..4b83cc9 100644 --- a/docs/user-guides/create-and-manage-volumes/with_the_dashboard/create-and-manage-volumes-with-the-dashboard.md +++ b/docs/user-guides/create-and-manage-volumes/with_the_dashboard/create-and-manage-volumes-with-the-dashboard.md @@ -6,7 +6,7 @@ label_names: - manage - dashboard position: 1 -title: Create and manage volumes via the dashboard +title: Create and Manage Volumes via the dashboard --- ## Create a volume diff --git a/docs/user-guides/index.md b/docs/user-guides/index.md index e05c652..9b3ad5c 100644 --- a/docs/user-guides/index.md +++ b/docs/user-guides/index.md @@ -24,6 +24,12 @@ Our platform's building blocks include: → Get started with our [Networks User Guides](create-and-manage-networks/index.md) +`Service Accounts` +: Use of Service Accounts on your projects versus personnal accounts. + + → Get started with our [Service Account User Guides](use-a-service-account/index.md) + + `Identity` : Identity management services to create application credentials and control access to projects. diff --git a/docs/user-guides/launch-and-manage-instances/default-user-nesi-images.md b/docs/user-guides/launch-and-manage-instances/default-user-nesi-images.md index 6c418bc..f2da7c8 100644 --- a/docs/user-guides/launch-and-manage-instances/default-user-nesi-images.md +++ b/docs/user-guides/launch-and-manage-instances/default-user-nesi-images.md @@ -4,7 +4,7 @@ label_names: - instance - launch position: 1 -title: Launch and manage instances +title: Launch and Manage Instances vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/index.md b/docs/user-guides/launch-and-manage-instances/index.md index 387f31f..c97b5b8 100644 --- a/docs/user-guides/launch-and-manage-instances/index.md +++ b/docs/user-guides/launch-and-manage-instances/index.md @@ -4,7 +4,7 @@ label_names: - instance - launch position: 1 -title: Launch and manage instances +title: Launch and Manage Instances vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/with_the_CLI/launch-an-instance-from-an-image.md b/docs/user-guides/launch-and-manage-instances/with_the_CLI/launch-an-instance-from-an-image.md index 163ba24..c9bd8f0 100644 --- a/docs/user-guides/launch-and-manage-instances/with_the_CLI/launch-an-instance-from-an-image.md +++ b/docs/user-guides/launch-and-manage-instances/with_the_CLI/launch-an-instance-from-an-image.md @@ -6,7 +6,7 @@ label_names: - cli - image position: 2 -title: Launch an Instance from an Image +title: Launch an Instance from an image vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/with_the_CLI/manage-security-groups-of-an-Instance-via-the-cli.md b/docs/user-guides/launch-and-manage-instances/with_the_CLI/manage-security-groups-of-an-Instance-via-the-cli.md index caa8f48..900d1da 100644 --- a/docs/user-guides/launch-and-manage-instances/with_the_CLI/manage-security-groups-of-an-Instance-via-the-cli.md +++ b/docs/user-guides/launch-and-manage-instances/with_the_CLI/manage-security-groups-of-an-Instance-via-the-cli.md @@ -4,7 +4,7 @@ label_names: - instance - resize position: 2 -title: Manage Security Groups of an Instance via CLI +title: Manage Security Groups of an instance via CLI vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/launch-an-instance-via-dashboard.md b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/launch-an-instance-via-dashboard.md index 64da169..a3f9d34 100644 --- a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/launch-an-instance-via-dashboard.md +++ b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/launch-an-instance-via-dashboard.md @@ -4,7 +4,7 @@ label_names: - instance - launch position: 2 -title: Launch an Instance via Dashboard +title: Launch an Instance via the dashboard vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/manage-security-groups-of-an-Instance-via-the-dashboard.md b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/manage-security-groups-of-an-Instance-via-the-dashboard.md index 081071d..97e5d13 100644 --- a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/manage-security-groups-of-an-Instance-via-the-dashboard.md +++ b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/manage-security-groups-of-an-Instance-via-the-dashboard.md @@ -4,7 +4,7 @@ label_names: - instance - resize position: 2 -title: Manage Security Groups of an Instance via the Dashboard +title: Manage Security Groups of an instance via the dashboard vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/resizing-an-Instance-via-the-dashboard.md b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/resizing-an-Instance-via-the-dashboard.md index 1f45346..a53bfda 100644 --- a/docs/user-guides/launch-and-manage-instances/with_the_dashboard/resizing-an-Instance-via-the-dashboard.md +++ b/docs/user-guides/launch-and-manage-instances/with_the_dashboard/resizing-an-Instance-via-the-dashboard.md @@ -4,7 +4,7 @@ label_names: - instance - resize position: 2 -title: Resizing an Instance via the Dashboard +title: Resizing an Instance via the dashboard vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/upload-and-manage-images/index.md b/docs/user-guides/upload-and-manage-images/index.md index 48a5fce..7ca34b4 100644 --- a/docs/user-guides/upload-and-manage-images/index.md +++ b/docs/user-guides/upload-and-manage-images/index.md @@ -5,7 +5,7 @@ label_names: - upload - manage position: 1 -title: Upload and manage images +title: Upload and Manage Images vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/upload-and-manage-images/with-the-cli/managing-images-via-cli.md b/docs/user-guides/upload-and-manage-images/with-the-cli/managing-images-via-cli.md index 5ec5edb..8bdf170 100644 --- a/docs/user-guides/upload-and-manage-images/with-the-cli/managing-images-via-cli.md +++ b/docs/user-guides/upload-and-manage-images/with-the-cli/managing-images-via-cli.md @@ -6,7 +6,7 @@ label_names: - manage - CLI position: 1 -title: Managing images +title: Manage Images vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/upload-and-manage-images/with-the-cli/upload-an-image-via-cli.md b/docs/user-guides/upload-and-manage-images/with-the-cli/upload-an-image-via-cli.md index 77aee28..41c22fd 100644 --- a/docs/user-guides/upload-and-manage-images/with-the-cli/upload-an-image-via-cli.md +++ b/docs/user-guides/upload-and-manage-images/with-the-cli/upload-an-image-via-cli.md @@ -6,7 +6,7 @@ label_names: - manage - CLI position: 1 -title: Uploading images +title: Upload Images vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/upload-and-manage-images/with-the-dashboard/managing-images-via-the-dashboard.md b/docs/user-guides/upload-and-manage-images/with-the-dashboard/managing-images-via-the-dashboard.md index 0a61383..323ad76 100644 --- a/docs/user-guides/upload-and-manage-images/with-the-dashboard/managing-images-via-the-dashboard.md +++ b/docs/user-guides/upload-and-manage-images/with-the-dashboard/managing-images-via-the-dashboard.md @@ -5,7 +5,7 @@ label_names: - upload - manage position: 2 -title: Managing images +title: Manage Images vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/upload-and-manage-images/with-the-dashboard/upload-an-image-via-the-dashboard.md b/docs/user-guides/upload-and-manage-images/with-the-dashboard/upload-an-image-via-the-dashboard.md index 6d8622b..7a098be 100644 --- a/docs/user-guides/upload-and-manage-images/with-the-dashboard/upload-an-image-via-the-dashboard.md +++ b/docs/user-guides/upload-and-manage-images/with-the-dashboard/upload-an-image-via-the-dashboard.md @@ -5,7 +5,7 @@ label_names: - upload - manage position: 1 -title: Uploading images +title: Upload Images vote_count: 1 vote_sum: 1 --- diff --git a/docs/user-guides/use-a-service-account/.pages.yml b/docs/user-guides/use-a-service-account/.pages.yml new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/docs/user-guides/use-a-service-account/.pages.yml @@ -0,0 +1 @@ + diff --git a/docs/user-guides/use-a-service-account/index.md b/docs/user-guides/use-a-service-account/index.md new file mode 100644 index 0000000..a40eff4 --- /dev/null +++ b/docs/user-guides/use-a-service-account/index.md @@ -0,0 +1,172 @@ +--- +hidden: false +label_names: +- identity +- security +- service account +position: 1 +title: Use a Service Account +description: Build project resources with your team service account rather than a personal account, so that access survives when someone leaves the team. +vote_count: 1 +vote_sum: 1 +--- + +Build your project on an account that belongs to the project, not to a person. + +When a team member leaves and their account is removed, anything created under +that account goes with it. A service account is an OpenStack user that REANNZ support team +creates for your project and your team shares, so that ownership of instances, +keys and automation is never tied to whether one individual is still with the +organisation. + +!!! note + Service accounts are created on request. + [Contact the support team](mailto:support@cloud.nesi.org.nz?subject=Research%20Developer%20Cloud%20Service%20Account). + +## When an personnal account is deleted + +Instances belong to your **project**, but keypairs and application credentials +are scoped to the **user** who created them. Removing a person's account +therefore leaves the instances running, and breaks everything used to reach and +manage them. + +| What was created under a personal account | After the account is removed | +| --- | --- | +| Instances | Still running, and still billed to the project | +| The keypair record in the RDC | Gone, so you cannot see which key built the instance | +| That person's public key in `authorized_keys` | Still there, still valid, nobody managing it | +| Application credentials they created | Stop working, so automation and pipelines fail | +| Their password and dashboard access | Gone, along with any recovery path through it | + +The third row is the one that matters most: the departing person's key keeps +working until somebody removes it by hand from every instance. See +[Secure and Rotate SSH Keys](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md) +for how to do that. + +## Which credential to use + +A service account does not replace your own key. The two do different jobs, and +using both keeps day-to-day access attributable while keeping the project +recoverable. + +| Credential | Belongs to | Used for | Replace it when | +| --- | --- | --- | --- | +| Your personal SSH key | You | Day-to-day connections to instances | You leave, or a device is lost | +| Service account SSH key | The project | Building instances, automation, recovery access | Anyone who held it leaves | +| Service account password | The project | Dashboard sign-in, issuing application credentials | Anyone who held it leaves | + +In practice: build the instance with the service account keypair, then add each +team member's personal public key to `authorized_keys` for everyday use. A +departure then costs you one `authorized_keys` edit, not a rebuild. + +!!! warning + Do not use the service account for routine interactive work. Logs record + the account, not the person, so shared sign-ins make it impossible to tell + afterwards who ran a command. Reserve it for building resources, for + automation, and for recovery. + +## Request a service account + +Please [contact Support](mailto:support@nesi.org.nz) and include: + +- The project the account is for +- A name for the account, matching your project so it is recognisable +- Who should be able to use it, and who owns it +- What it will be used for, such as Terraform, a cluster, or shared instances + +## Set up the account + +Work through these once, when the account is first issued. + +1. Sign in to the [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/) as the + service account and set a new password: + + ``` { .sh } + openstack user password set --password NEW_PASSWORD --original-password OLD_PASSWORD + ``` + +2. Record the new password in your team password manager, not anywhere else. + +3. Generate an SSH key for the account on a workstation, following + [Create a protected key](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#create-a-protected-key). + Give it a passphrase, and store the private key and its passphrase in the + same password manager entry. + +4. Import the public key while authenticated as the service account: + + ``` { .sh } + openstack keypair create --public-key ~/.ssh/id_svc_project.pub project-svc-2026-08 + ``` + +5. Build instances with that keypair, then add each person's personal public key + to `authorized_keys` for day-to-day access. + +6. Create [application credentials](index.md) under the service account for any + automation, so pipelines no longer depend on an individual. + +## Who may hold the secrets + +The service account is only as good as the discipline around its secrets. A +shared credential that everyone can read is worse than a personal one, because +nothing ties an action back to a person. + +- Keep the password, private key and passphrase in a **team password manager**, + with a separate login per person. Access is then granted and revoked per + person without changing the secret itself +- Never put them in a repository, chat message, email, ticket or shared drive +- Grant access to the smallest group that needs it, and keep a written list of + who currently holds it. You will need that list the day someone leaves +- Review the list whenever the team changes, and at least once a year +- Update the password when a team member leaves and rotate it regularly + +## When someone leaves + +Work through this on their last day. Steps 1 and 2 cover any departure. Steps 3 +onwards apply when the person had access to the service account secrets. + +1. Remove their personal public key from `authorized_keys` on every instance, + following + [Rotate a key step by step](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#rotate-a-key-step-by-step) +2. Revoke their login to the team password manager +3. Recreate the service account password, because they knew the old one: + + ``` { .sh } + openstack user password set --password NEW_PASSWORD --original-password OLD_PASSWORD + ``` + +4. Rotate the service account SSH key, because they held a copy of the private + key, then update the new password and key in the password manager +5. Delete and recreate any application credentials they could have copied. A new + password does **not** invalidate them, because each has its own secret +6. Check for anything still owned by their personal account, such as keypairs, + application credentials or instances they built before the team moved to the + service account +7. Record what was rotated, and when + +!!! warning + Step 5 is the one most often missed. Application credential secrets are + independent of the account password, so a credential copied before someone + left keeps working until it is deleted. List them with + `openstack application credential list` while signed in as the service + account. + +## Move an existing project + +If your instances were built with a personal key, you do not need to rebuild +them to change over. + +1. Request the service account, and set it up as above +2. Add the service account's public key to `authorized_keys` on every existing + instance, using steps 3 and 4 of + [Rotate a key step by step](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#rotate-a-key-step-by-step) +3. Recreate application credentials under the service account, and update the + automation that uses them +4. Build all new instances with the service account keypair +5. Once everything is reachable through the service account, remove the personal + keys that were being used for ownership rather than day-to-day access + +!!! note + A keypair cannot be transferred between accounts. Import the same public key + under the service account, or generate a new one, then remove the old + keypair record from the personal account. + From e81629ab3e8a2912672b6a8850181db07f3c689f Mon Sep 17 00:00:00 2001 From: Kahu Anderson Date: Thu, 13 Aug 2026 10:48:33 +1200 Subject: [PATCH 2/2] Pages rework and updates to bring the pages in line with the rest of the layouts and docs --- CLAUDE.md | 127 +++++ docs/user-guides/.pages.yml | 23 +- .../create-and-manage-identity/index.md | 50 +- .../use-a-service-account.md} | 108 ++-- .../create-and-manage-keypairs/.pages.yml | 8 + .../if-a-key-is-compromised.md | 69 +++ .../create-and-manage-keypairs/index.md | 51 +- .../rotate-ssh-keys.md | 279 +++++++++++ .../secure-and-rotate-ssh-keys.md | 471 ------------------ .../secure-your-ssh-keys.md | 165 ++++++ docs/user-guides/index.md | 14 +- .../use-a-service-account/.pages.yml | 1 - 12 files changed, 775 insertions(+), 591 deletions(-) create mode 100644 CLAUDE.md rename docs/user-guides/{use-a-service-account/index.md => create-and-manage-identity/use-a-service-account.md} (56%) create mode 100644 docs/user-guides/create-and-manage-keypairs/.pages.yml create mode 100644 docs/user-guides/create-and-manage-keypairs/if-a-key-is-compromised.md create mode 100644 docs/user-guides/create-and-manage-keypairs/rotate-ssh-keys.md delete mode 100644 docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md create mode 100644 docs/user-guides/create-and-manage-keypairs/secure-your-ssh-keys.md delete mode 100644 docs/user-guides/use-a-service-account/.pages.yml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4b1b919 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,127 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Content-only MkDocs Material site for the NeSI / REANNZ **Research Developer Cloud** (RDC), an +OpenStack platform. Published to via GitHub Pages. There is no +application code here — the "source" is Markdown in `docs/`, plus a small Python toolchain that +lints it and builds the site. + +Almost every change is a documentation change. Treat prose quality and voice as the primary concern, +and the build config as secondary. + +## Commands + +```sh +pip install -r requirements.txt # full pinned toolchain (pip-compile from requirements.in) +mkdocs serve # local preview at http://localhost:8000/ +mkdocs build --clean # writes to public/ (site_dir), not site/ +``` + +QA checks. Each takes a space-delimited file list; pass `docs/**/*.md` to lint everything. CI runs +these on changed files for any push to a non-`main` branch. + +```sh +python3 checks/run_spell_check.py docs/path/to/page.md # pyspelling + aspell, needs `apt install aspell` +python3 checks/run_proselint.py docs/path/to/page.md +python3 checks/run_meta_check.py docs/path/to/page.md # frontmatter + heading rules +markdownlint --config .markdownlint.json --json docs/path/to/page.md 2>&1 | python3 checks/parse_markdownlint.py +./checks/run_test_build.py # strict-ish build, surfaces mkdocs warnings +``` + +Checks also have VSCode debug jobs (`.vscode/tasks.json`), most of which run against +`checks/fail_checks.md` — a deliberately broken page used as a fixture. Don't "fix" it. + +Deploy is automatic on push to `main` (`.github/workflows/deploy.yml`). Never build to `gh-pages` by +hand. + +## Architecture + +**Nav is not in `mkdocs.yml`.** It comes from `mkdocs-awesome-nav` reading `.pages.yml` files +scattered through `docs/`. To place a new page or section, edit the `.pages.yml` in its parent +directory. A trailing `- "*"` means "then everything else, alphabetically". An empty `.pages.yml` +(e.g. `docs/user-guides/set-up-your-cli-environment/`) leaves that folder's ordering to the plugin. +Directory structure defines sections; `index.md` is that section's landing page. + +**Two hook files, easily confused:** + +- `macro_hooks.py` → `define_env()`, injects variables usable in **Markdown** via `{{ ... }}` + (mkdocs-macros). Currently exposes `applications` from `docs/assets/module-list.json`. +- `mkdocs_hooks.py` → injects variables into **Jinja templates** in `overrides/*.html`. + +Because mkdocs-macros processes every page, literal `{{ }}` in a code block must be wrapped in +`{% raw %}` / `{% endraw %}` or the build fails (`on_error_fail: true`). See +`docs/user-guides/create-and-manage-keypairs/rotate-ssh-keys.md` for the one existing example. + +**Externally-managed assets.** `docs/assets/glossary/dictionary.txt` and `docs/assets/module-list.json` +are pulled in by CI from the `nesi/nesi-wordlist` and `nesi/modules-list` repos. Do not hand-edit the +dictionary to silence a spellcheck failure — fix the spelling, or raise it upstream. + +`redirect_map.yml` exists but the `redirects` plugin is **not** enabled in `mkdocs.yml`. Renaming or +moving a page currently breaks its published URL; call that out rather than assuming a redirect +catches it. + +## Page conventions + +Frontmatter, as enforced by `checks/run_meta_check.py`: + +```yaml +--- +hidden: false +label_names: # at least one; the vocabulary is checks/.approved_tags.yml +- keypairs +- security +position: 1 +title: Create and Manage Keypairs # ≤ 28 chars, title case +description: One sentence, used in search results and cards +--- +``` + +`index.md` files are exempt from the meta checks, which is why many carry no `description`. + +Other conventions: + +- Code fences carry a language attribute: `` ``` { .sh } `` for commands, `` ``` { .sh .no-copy } `` + for output the reader should not copy. The `.no-copy` variant is the more common of the two. +- Cross-page links are relative and point at the `.md` file, not the built URL. +- Callouts use `!!! note` for context and caveats. `!!! warning` is rare — reserve it for genuinely + destructive actions (data loss, lockout). Most pages have zero; the corpus norm is at most two. +- New Zealand English throughout (organisation, authorised, visualisation). +- Support address is `support@cloud.nesi.org.nz`. + +**Naming is inconsistent and it is not your job to fix it wholesale.** The platform is called +"FlexiHPC" on ~123 lines and "RDC" / "Research Developer Cloud" on ~28. Newer pages prefer RDC. Match +the page you are editing; don't rename across the repo without being asked. + +## Voice + +This is the part most likely to go wrong. The house style is **descriptive and permissive** — explain +what a thing is and what will happen, then let the reader decide. It reads as a platform team sharing +what it knows, consistent with the shared-responsibility framing in `docs/security/`. + +Characteristic constructions: + +- "You **are able to** create a new SSH Key pair on the RDC or import one of your own." +- "Key pairs **can be** managed a few ways" +- "This **allows you to** adopt the principle of least privilege" +- "You **will need to** ensure you have created and assigned a Security group" + +Bare imperatives are fine *inside* a numbered step the reader has already committed to ("Copy the +floating ip address", "run the following command"). They are out of place in prose about choices. + +Avoid: + +- Policy vocabulary — "never", "do not", "you must", "grant access to the smallest group". + NeSI documents the platform; it does not write the reader's internal policy. +- Headings that grant or withhold permission ("Who may hold the secrets"). Name the topic instead. +- Instructions about the reader's organisation or calendar — "on their last day", "at least once a + year", "record what was rotated". +- Conduct checklists. Tables here carry reference data (flavours, default users, image formats), not + rubrics. +- A more literary register than the surrounding pages. The corpus is plain and workmanlike. + +The reliable transform is **prescription → consequence**: instead of "Set a passphrase on every +private key", write "A private key with a passphrase is useless to anyone who copies the file", and +let the reader draw the conclusion. diff --git a/docs/user-guides/.pages.yml b/docs/user-guides/.pages.yml index 6d9aed8..ed7c796 100644 --- a/docs/user-guides/.pages.yml +++ b/docs/user-guides/.pages.yml @@ -1,12 +1,11 @@ -nav: - - index.md - - launch-and-manage-instances - - upload-and-manage-images - - create-and-manage-volumes - - create-and-manage-networks - - use-a-service-account - - create-and-manage-identity - - create-and-manage-keypairs - - create-and-manage-object-storage - - set-up-your-cli-environment - - "*" +nav: + - index.md + - launch-and-manage-instances + - upload-and-manage-images + - create-and-manage-volumes + - create-and-manage-networks + - create-and-manage-identity + - create-and-manage-keypairs + - create-and-manage-object-storage + - set-up-your-cli-environment + - "*" diff --git a/docs/user-guides/create-and-manage-identity/index.md b/docs/user-guides/create-and-manage-identity/index.md index 40f0803..2460966 100644 --- a/docs/user-guides/create-and-manage-identity/index.md +++ b/docs/user-guides/create-and-manage-identity/index.md @@ -1,24 +1,26 @@ ---- -hidden: false -label_names: -- identity -- create -- manage -position: 1 -title: Create and Manage Identity -vote_count: 1 -vote_sum: 1 ---- - -`Application Credentials` help you to avoid the practice of embedding user account credentials in configuration files. Instead, the user creates an Application Credential that receives delegated access to a single project and has its own distinct secret. The user can also limit the delegated privileges to a single role in that project. This allows you to adopt the principle of least privilege, where the authenticated service only gains access to the one project and role that it needs to function, rather than all of them. - -This approach allows you to consume an API without revealing your user credentials, and lets applications authenticate to Keystone without requiring embedded user credentials. - -Within FlexiHPC you are able to mange `Application Credentials` from the dashboard and/or the CLI. - -- [Creating and Managing Application Credentials from the dashboard](creating-and-managing-application-credentials-with-the-dashboard.md) - -- [Creating and Managing Application Credentials via CLI](creating-and-managing-application-credentials-via-cli.md) - -!!! note - The Application Credential is dependent on the user account that created it, so it will terminate if that account is ever deleted, or loses access to the relevant role. +--- +hidden: false +label_names: +- identity +- create +- manage +position: 1 +title: Create and Manage Identity +vote_count: 1 +vote_sum: 1 +--- + +`Application Credentials` help you to avoid the practice of embedding user account credentials in configuration files. Instead, the user creates an Application Credential that receives delegated access to a single project and has its own distinct secret. The user can also limit the delegated privileges to a single role in that project. This allows you to adopt the principle of least privilege, where the authenticated service only gains access to the one project and role that it needs to function, rather than all of them. + +This approach allows you to consume an API without revealing your user credentials, and lets applications authenticate to Keystone without requiring embedded user credentials. + +Within FlexiHPC you are able to mange `Application Credentials` from the dashboard and/or the CLI. + +- [Creating and Managing Application Credentials from the dashboard](creating-and-managing-application-credentials-with-the-dashboard.md) + +- [Creating and Managing Application Credentials via CLI](creating-and-managing-application-credentials-via-cli.md) + +- [Use a Service Account](use-a-service-account.md) + +!!! note + The Application Credential is dependent on the user account that created it, so it will terminate if that account is ever deleted, or loses access to the relevant role. diff --git a/docs/user-guides/use-a-service-account/index.md b/docs/user-guides/create-and-manage-identity/use-a-service-account.md similarity index 56% rename from docs/user-guides/use-a-service-account/index.md rename to docs/user-guides/create-and-manage-identity/use-a-service-account.md index a40eff4..1b8e19a 100644 --- a/docs/user-guides/use-a-service-account/index.md +++ b/docs/user-guides/create-and-manage-identity/use-a-service-account.md @@ -6,24 +6,24 @@ label_names: - service account position: 1 title: Use a Service Account -description: Build project resources with your team service account rather than a personal account, so that access survives when someone leaves the team. +description: How a shared service account keeps ownership of instances, keys and automation with the project rather than with one person. vote_count: 1 vote_sum: 1 --- -Build your project on an account that belongs to the project, not to a person. +A service account is an OpenStack user that belongs to your project rather than +to a person. The REANNZ support team creates it on request, and your team +shares it, so that ownership of instances, keys and automation is not tied to +whether one individual is still with the organisation. When a team member leaves and their account is removed, anything created under -that account goes with it. A service account is an OpenStack user that REANNZ support team -creates for your project and your team shares, so that ownership of instances, -keys and automation is never tied to whether one individual is still with the -organisation. +that account goes with it. !!! note - Service accounts are created on request. + Service accounts are created on request. [Contact the support team](mailto:support@cloud.nesi.org.nz?subject=Research%20Developer%20Cloud%20Service%20Account). -## When an personnal account is deleted +## When an account is deleted Instances belong to your **project**, but keypairs and application credentials are scoped to the **user** who created them. Removing a person's account @@ -38,9 +38,9 @@ manage them. | Application credentials they created | Stop working, so automation and pipelines fail | | Their password and dashboard access | Gone, along with any recovery path through it | -The third row is the one that matters most: the departing person's key keeps -working until somebody removes it by hand from every instance. See -[Secure and Rotate SSH Keys](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md) +The third row is the one that catches people out: the departing person's key +keeps working until somebody removes it by hand from every instance. See +[Rotate SSH Keys](../create-and-manage-keypairs/rotate-ssh-keys.md) for how to do that. ## Which credential to use @@ -49,25 +49,26 @@ A service account does not replace your own key. The two do different jobs, and using both keeps day-to-day access attributable while keeping the project recoverable. -| Credential | Belongs to | Used for | Replace it when | +| Credential | Belongs to | Used for | Replaced when | | --- | --- | --- | --- | | Your personal SSH key | You | Day-to-day connections to instances | You leave, or a device is lost | | Service account SSH key | The project | Building instances, automation, recovery access | Anyone who held it leaves | | Service account password | The project | Dashboard sign-in, issuing application credentials | Anyone who held it leaves | -In practice: build the instance with the service account keypair, then add each -team member's personal public key to `authorized_keys` for everyday use. A -departure then costs you one `authorized_keys` edit, not a rebuild. +A common arrangement is to build the instance with the service account keypair, +then add each team member's personal public key to `authorized_keys` for +everyday use. A departure then costs one `authorized_keys` edit rather than a +rebuild. -!!! warning - Do not use the service account for routine interactive work. Logs record - the account, not the person, so shared sign-ins make it impossible to tell - afterwards who ran a command. Reserve it for building resources, for - automation, and for recovery. +!!! note + Logs record the account rather than the person, so a shared sign-in makes it + hard to tell afterwards who ran a command. The service account is most + useful for building resources, for automation and for recovery, with + personal accounts used for interactive work. ## Request a service account -Please [contact Support](mailto:support@nesi.org.nz) and include: +Please [contact Support](mailto:support@cloud.nesi.org.nz) and include: - The project the account is for - A name for the account, matching your project so it is recognisable @@ -76,19 +77,22 @@ Please [contact Support](mailto:support@nesi.org.nz) and include: ## Set up the account -Work through these once, when the account is first issued. +These steps are done once, when the account is first issued. -1. Sign in to the [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/) as the - service account and set a new password: +1. Set a new password on the account, replacing the one support supplied. While + authenticated as the service account: ``` { .sh } openstack user password set --password NEW_PASSWORD --original-password OLD_PASSWORD ``` -2. Record the new password in your team password manager, not anywhere else. + The password can also be changed from the + [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/). + +2. Record the new password in your team password manager. 3. Generate an SSH key for the account on a workstation, following - [Create a protected key](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#create-a-protected-key). + [Create a protected key](../create-and-manage-keypairs/secure-your-ssh-keys.md#create-a-protected-key). Give it a passphrase, and store the private key and its passphrase in the same password manager entry. @@ -101,32 +105,37 @@ Work through these once, when the account is first issued. 5. Build instances with that keypair, then add each person's personal public key to `authorized_keys` for day-to-day access. -6. Create [application credentials](index.md) under the service account for any - automation, so pipelines no longer depend on an individual. +6. Create [application credentials](index.md) + under the service account for any automation, so pipelines no longer depend + on an individual. -## Who may hold the secrets +## Sharing the account secrets -The service account is only as good as the discipline around its secrets. A -shared credential that everyone can read is worse than a personal one, because -nothing ties an action back to a person. +A shared credential is only as useful as the record of who can read it. Where +everyone has a copy and nobody has a list, nothing ties an action back to a +person. -- Keep the password, private key and passphrase in a **team password manager**, - with a separate login per person. Access is then granted and revoked per - person without changing the secret itself -- Never put them in a repository, chat message, email, ticket or shared drive -- Grant access to the smallest group that needs it, and keep a written list of - who currently holds it. You will need that list the day someone leaves -- Review the list whenever the team changes, and at least once a year -- Update the password when a team member leaves and rotate it regularly +- A **team password manager** with a separate login per person holds the + password, private key and passphrase. Access can then be granted and revoked + per person without changing the secret itself +- A secret that has been in a repository, chat message, email, ticket or shared + drive should be treated as exposed, and replaced +- A written list of who currently holds the secrets is what makes the checklist + below workable. The shorter that list, the less there is to rotate, and it is + worth revisiting whenever the team changes +- The service account password and key rotate on the same triggers as any other + key. See + [When to rotate a key](../create-and-manage-keypairs/rotate-ssh-keys.md#when-to-rotate-a-key) ## When someone leaves -Work through this on their last day. Steps 1 and 2 cover any departure. Steps 3 -onwards apply when the person had access to the service account secrets. +These are the things that keep working until they are changed. Steps 1 and 2 +apply to any departure. Steps 3 onwards apply where the person had access to the +service account secrets. 1. Remove their personal public key from `authorized_keys` on every instance, following - [Rotate a key step by step](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#rotate-a-key-step-by-step) + [Rotate a key step by step](../create-and-manage-keypairs/rotate-ssh-keys.md#rotate-a-key-step-by-step) 2. Revoke their login to the team password manager 3. Recreate the service account password, because they knew the old one: @@ -141,9 +150,10 @@ onwards apply when the person had access to the service account secrets. 6. Check for anything still owned by their personal account, such as keypairs, application credentials or instances they built before the team moved to the service account -7. Record what was rotated, and when -!!! warning +A note of what was rotated, and when, saves working it out later. + +!!! note Step 5 is the one most often missed. Application credential secrets are independent of the account password, so a credential copied before someone left keeps working until it is deleted. List them with @@ -158,15 +168,15 @@ them to change over. 1. Request the service account, and set it up as above 2. Add the service account's public key to `authorized_keys` on every existing instance, using steps 3 and 4 of - [Rotate a key step by step](../create-and-manage-keypairs/secure-and-rotate-ssh-keys.md#rotate-a-key-step-by-step) + [Rotate a key step by step](../create-and-manage-keypairs/rotate-ssh-keys.md#rotate-a-key-step-by-step) 3. Recreate application credentials under the service account, and update the automation that uses them 4. Build all new instances with the service account keypair -5. Once everything is reachable through the service account, remove the personal - keys that were being used for ownership rather than day-to-day access +5. Once everything is reachable through the service account, the personal keys + that were being used for ownership rather than day-to-day access can be + removed !!! note A keypair cannot be transferred between accounts. Import the same public key under the service account, or generate a new one, then remove the old keypair record from the personal account. - diff --git a/docs/user-guides/create-and-manage-keypairs/.pages.yml b/docs/user-guides/create-and-manage-keypairs/.pages.yml new file mode 100644 index 0000000..48d13c4 --- /dev/null +++ b/docs/user-guides/create-and-manage-keypairs/.pages.yml @@ -0,0 +1,8 @@ +nav: + - index.md + - create-and-manage-keypairs-with-the-dashboard.md + - create-and-manage-keypairs-via-cli.md + - secure-your-ssh-keys.md + - rotate-ssh-keys.md + - if-a-key-is-compromised.md + - "*" diff --git a/docs/user-guides/create-and-manage-keypairs/if-a-key-is-compromised.md b/docs/user-guides/create-and-manage-keypairs/if-a-key-is-compromised.md new file mode 100644 index 0000000..7048143 --- /dev/null +++ b/docs/user-guides/create-and-manage-keypairs/if-a-key-is-compromised.md @@ -0,0 +1,69 @@ +--- +hidden: false +label_names: +- keypairs +- security +- ssh +position: 3 +title: If a Key Is Compromised +description: Checks to run on an instance when an SSH key may be in someone else's hands, and what to rotate alongside the key. +--- + +A compromised key usually means the instances it reached are compromised too, +so the checks below apply to every host the key could reach. + +Rotation is the first step, and does not wait for a maintenance window. See +[Rotate SSH Keys](rotate-ssh-keys.md) for the procedure. + +## Check what the host trusts + +Keys you did not add are a common way of keeping access: + +``` { .sh } +ssh-keygen -lf ~/.ssh/authorized_keys +sudo ssh-keygen -lf /root/.ssh/authorized_keys +``` + +## Check what has been used + +The journal records which keys have been used to log in, and from where: + +``` { .sh } +sudo journalctl -u ssh --grep "Accepted publickey" +``` + +Each line records the fingerprint that was accepted, so you can tell which +sessions were yours and which were not. On older images that still write a text +log, use `sudo grep "Accepted publickey" /var/log/auth.log` instead. + +## Check what the daemon allows + +The daemon may also be accepting passwords: + +``` { .sh } +sudo sshd -T | grep -E "permitrootlogin|passwordauthentication" +``` + +`passwordauthentication` should read `no`. `permitrootlogin` reads +`prohibit-password` on the Ubuntu cloud images, which allows `root` in by key +only, and `no` is the safer setting unless something genuinely needs `root` +over SSH. + +## Then the rest + +- Rotate the key, following [Rotate SSH Keys](rotate-ssh-keys.md) +- Rotate any [application credentials](../create-and-manage-identity/index.md) + and EC2 credentials that the instance or the person held +- An instance you cannot fully account for is safer rebuilt than cleaned +- [Security groups](../create-and-manage-networks/index.md) can limit port 22 to + the addresses you expect +- Please [contact Support](mailto:support@cloud.nesi.org.nz) if data may have + been exposed + +!!! note + An `authorized_keys` entry can be restricted to the addresses it will be + accepted from, which limits the value of a stolen key: + + ``` { .sh .no-copy } + from="10.0.0.0/24",restrict ssh-ed25519 AAAAC3Nz... name@laptop-2026-08 + ``` diff --git a/docs/user-guides/create-and-manage-keypairs/index.md b/docs/user-guides/create-and-manage-keypairs/index.md index 34b1de8..9c7ab87 100644 --- a/docs/user-guides/create-and-manage-keypairs/index.md +++ b/docs/user-guides/create-and-manage-keypairs/index.md @@ -1,24 +1,27 @@ ---- -hidden: false -label_names: -- identity -- create -- manage -position: 1 -title: Create and Manage Keypairs -vote_count: 1 -vote_sum: 1 ---- - -Key pairs are SSH credentials that are injected into a FlexiHPC instance when it is launched. These are used to access and manage your instances. - -You are able to create a new SSH Key pair on the RDC or import one of your own. - - -Key pairs can be managed a few ways - -- [Create and manage key pairs via the dashboard](create-and-manage-keypairs-with-the-dashboard.md) - -- [Create and manage key pairs via CLI](create-and-manage-keypairs-via-cli.md) - -- [Secure and rotate ssh keys](secure-and-rotate-ssh-keys.md) +--- +hidden: false +label_names: +- identity +- create +- manage +position: 1 +title: Create and Manage Keypairs +vote_count: 1 +vote_sum: 1 +--- + +Key pairs are SSH credentials that are injected into a FlexiHPC instance when it is launched. These are used to access and manage your instances. + +You are able to create a new SSH Key pair on the RDC or import one of your own. + +Key pairs can be managed a few ways + +- [Create and manage key pairs via the dashboard](create-and-manage-keypairs-with-the-dashboard.md) + +- [Create and manage key pairs via CLI](create-and-manage-keypairs-via-cli.md) + +- [Secure your SSH keys](secure-your-ssh-keys.md) + +- [Rotate SSH keys](rotate-ssh-keys.md) + +- [If a key is compromised](if-a-key-is-compromised.md) diff --git a/docs/user-guides/create-and-manage-keypairs/rotate-ssh-keys.md b/docs/user-guides/create-and-manage-keypairs/rotate-ssh-keys.md new file mode 100644 index 0000000..e667376 --- /dev/null +++ b/docs/user-guides/create-and-manage-keypairs/rotate-ssh-keys.md @@ -0,0 +1,279 @@ +--- +hidden: false +label_names: +- keypairs +- security +- ssh +position: 2 +title: Rotate SSH Keys +description: Replace an SSH key on the Research Developer Cloud, across single instances and whole clusters, without losing access to what is already running. +--- + +Replacing an SSH key means adding the new one and removing the old one from +every host that trusts it. The RDC keypair record is only part of that, and not +the part that controls access. + +!!! warning + Deleting a keypair in the RDC does **not** revoke access to instances that + are already running. OpenStack uses the keypair record only to inject your + public key when an instance is **first built**. After that the key lives in + `~/.ssh/authorized_keys` on the instance and OpenStack no longer tracks it. + To retire a key you must remove it from `authorized_keys` on every instance + that has it. + +An instance keeps trusting a key until it is removed from that instance, which +is why the procedure below works the way it does. + +## What each action affects + +| Action | Running instances | New instances | +| --- | --- | --- | +| Upload a new public key to the RDC | No effect | Get the new key | +| Delete a keypair in the RDC | No effect | Cannot use that name | +| Edit `authorized_keys` on a host | Takes effect at once | No effect | +| Rebuild an instance with a new key | That one instance gets the new key, and is reinstalled | No effect | + +## When to rotate a key + +Rotation is usually on a routine cycle, brought forward by any of these events. + +| Trigger | Urgency | +| --- | --- | +| Routine cycle | Every 12 months | +| Someone leaves the team or project | Same day, see [When someone leaves](../create-and-manage-identity/use-a-service-account.md#when-someone-leaves) | +| A laptop or phone is lost or stolen | Immediately | +| A private key was sent over email or chat | Immediately | +| A key was stored without a passphrase on a shared machine | Immediately | +| An instance shows signs of compromise | Immediately, and see [If a Key Is Compromised](if-a-key-is-compromised.md) | +| A key was shared between people | At the next window, then stop sharing | + +## Rotate a key step by step + +The order matters: the new key is added and tested before the old one is +removed, so a mistake at any point still leaves you a way in. + +!!! note + Before you start, confirm you have a second way in: console access through + the [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/), or a colleague + with their own working key. Recovering an instance whose only key is gone + is slow. + +### 1. Check what you have + +List the keypairs on your account and note the name, type and fingerprint of +the one you are replacing: + +``` { .sh } +openstack keypair list +``` + +``` { .sh .no-copy } ++-----------------+-------------------------------------------------+------+ +| Name | Fingerprint | Type | ++-----------------+-------------------------------------------------+------+ +| name-rdc-2025-03 | f0:4f:53:d1:3d:aa:71:d9:ef:b6:32:22:88:c2:68:42 | ssh | ++-----------------+-------------------------------------------------+------+ +``` + +Then list every instance that was built with it, so you know the full set of +hosts to update: + +``` { .sh } +openstack server list --long -c Name -c "Key Name" -c Networks +``` + +Add anything the RDC cannot see: nodes reached through a bastion, hosts you +added the key to by hand, and any automation that holds a copy. + +### 2. Generate the new key + +Follow +[Create a protected key](secure-your-ssh-keys.md#create-a-protected-key), giving +the new key its own filename and a comment that dates it: + +``` { .sh } +ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_2026_08 +``` + +Keep the old key in place for now. You need it to install the new one. + +### 3. Add the new key + +Append the new public key to `authorized_keys` on every host from step 1, +authenticating with the old key. The new key is added alongside the old, so +nothing breaks yet: + +``` { .sh } +NEWPUB=$(cat ~/.ssh/id_rdc_2026_08.pub) +ssh -i ~/.ssh/id_rdc_2025_03 ubuntu@INSTANCE_IP \ + "umask 077; mkdir -p ~/.ssh; grep -qxF '$NEWPUB' ~/.ssh/authorized_keys \ + || echo '$NEWPUB' >> ~/.ssh/authorized_keys" +``` + +The `grep -qxF` test makes the command safe to run twice: it appends the key +only if it is not already there. + +Repeat for every host. For more than a handful, see +[Rotate keys on a cluster](#rotate-keys-on-a-cluster). + +### 4. Test the new key + +Leave your current session open. In a **second terminal**, connect with the new +key only: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@INSTANCE_IP +``` + +`IdentitiesOnly=yes` stops SSH quietly falling back to the old key and giving +you a false pass. You should be prompted for the new passphrase, then let in. + +The remaining steps assume this has succeeded on **every** host. + +### 5. Remove the old key + +Removing the entry from `authorized_keys` is what actually revokes the old key. +Match on the key body rather than the comment, which anyone can edit: + +``` { .sh } +OLDPUB=$(awk '{print $2}' ~/.ssh/id_rdc_2025_03.pub) +ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP \ + "grep -v -F '$OLDPUB' ~/.ssh/authorized_keys > ~/.ssh/ak.new \ + && mv ~/.ssh/ak.new ~/.ssh/authorized_keys \ + && chmod 600 ~/.ssh/authorized_keys" +``` + +Confirm what is left, and that you recognise all of it: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP "ssh-keygen -lf ~/.ssh/authorized_keys" +``` + +Other accounts on the host hold their own `authorized_keys`, `root` in +particular: + +``` { .sh } +sudo ssh-keygen -lf /root/.ssh/authorized_keys +``` + +The old private key can come off your workstation once every host is done. + +### 6. Update OpenStack + +Import the new public key so that instances built from now on receive it: + +``` { .sh } +openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub name-rdc-2026-08 +openstack keypair delete name-rdc-2025-03 +``` + +A dated name keeps the two keys visible side by side and makes the change +obvious in Terraform or Heat plans. + +!!! note + If the keypair name is pinned in a template you cannot easily edit, reuse + it instead. OpenStack keypairs cannot be updated in place, so delete and + re-create: + + ``` { .sh } + openstack keypair delete KEY_PAIR_NAME + openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub KEY_PAIR_NAME + ``` + + Between the two commands the name does not exist, so any automation that + builds instances will fail until the second command completes. + +Confirm the fingerprint in the RDC matches the key on your workstation: + +``` { .sh } +openstack keypair list +ssh-keygen -lf ~/.ssh/id_rdc_2026_08.pub -E md5 +``` + +### 7. Record the change + +Worth noting in your team documentation or ticket system: + +- The date, and who performed the rotation +- Old and new key names, and their fingerprints +- Which hosts were updated, and any that were skipped +- Why the key was rotated, if it was not the routine cycle +- The date the next routine rotation is due + +Fingerprints are the useful part: they let anyone confirm later which key a +host actually trusts. + +## Rotate keys on a cluster + +Kubernetes and other multi-node clusters need the same steps applied to every +node. Two approaches scale better than editing nodes by hand. + +### Update every node + +Configuration management makes the result repeatable and easy to audit. The +Ansible `authorized_key` module with `exclusive: true` sets the file to exactly +the keys you list, which handles the add and the remove in one pass: + +{% raw %} + +``` { .yaml } +- name: Set the authorised keys for the ubuntu user + ansible.posix.authorized_key: + user: ubuntu + state: present + exclusive: true + key: "{{ lookup('file', '~/.ssh/id_rdc_2026_08.pub') }}" +``` + +{% endraw %} + +!!! warning + `exclusive: true` removes every other key for that user, including any + added by a platform tool or a colleague. Run it against one node first, and + check the result before you run it against the rest. + +Reach nodes on a private network through the bastion: + +``` { .sh } +ansible-playbook -i inventory.ini rotate-keys.yml \ + --ssh-common-args '-o ProxyJump=ubuntu@BASTION_IP' +``` + +### Replace nodes instead + +Replacing nodes is usually cleaner than editing them. A node built after the +keypair is updated comes up with only the new key, so there is nothing to +remove and no chance of a missed host. + +Drain and replace one node at a time so the workload stays up: + +``` { .sh } +kubectl drain NODE_NAME --ignore-daemonsets --delete-emptydir-data +``` + +Delete the node through your cluster tooling, let the group build a +replacement, then confirm it before moving on: + +``` { .sh } +kubectl get nodes +``` + +!!! warning + `openstack server rebuild --key-name NEW_KEY SERVER` also resets the key, + but it rebuilds the instance from its image and destroys everything on the + root disk. Only use it where the instance holds no state. + +### New nodes and scaling + +Editing `authorized_keys` on a running node changes that node alone. When the +cluster scales up, or replaces a failed node, the replacement is built from the +keypair recorded in the cluster or node-group definition. + +Updating that definition as well as the nodes is what stops the old key +reappearing on the next node built. After a scaling event, a new node can be +checked with: + +``` { .sh } +ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@NEW_NODE_IP \ + "ssh-keygen -lf ~/.ssh/authorized_keys" +``` diff --git a/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md b/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md deleted file mode 100644 index a923f71..0000000 --- a/docs/user-guides/create-and-manage-keypairs/secure-and-rotate-ssh-keys.md +++ /dev/null @@ -1,471 +0,0 @@ ---- -hidden: false -label_names: -- keypairs -- security -- ssh -position: 1 -title: Secure and Rotate SSH Keys -description: Protect an SSH private key with a passphrase, and rotate a key on the Research Developer Cloud without losing access to your running instances. ---- - -An SSH private key is the credential that stands between the public internet and -your instances. Unlike a password it never expires, nothing prompts you to -change it, and a forgotten copy can sit on an old laptop for years. Treat the -private key as you would treat a password: protect it, know where every copy -is, and replace it on a schedule. - -This page explains how to protect a key, then gives a step-by-step procedure -for rotating one. - -!!! warning - Deleting a keypair in the RDC does **not** revoke access to instances that - are already running. OpenStack uses the keypair record only to inject your - public key when an instance is **first built**. After that the key lives in - `~/.ssh/authorized_keys` on the instance and OpenStack no longer tracks it. - To retire a key you must remove it from `authorized_keys` on every instance - that has it. - -That single fact shapes the whole procedure below: an instance keeps trusting a -key until you go to the instance and remove it. - -## What each action affects - -| Action | Running instances | New instances | -| --- | --- | --- | -| Upload a new public key to the RDC | No effect | Get the new key | -| Delete a keypair in the RDC | No effect | Cannot use that name | -| Edit `authorized_keys` on a host | Takes effect at once | No effect | -| Rebuild an instance with a new key | That one instance gets the new key, and is reinstalled | No effect | - -## Good practice checklist - -| Practice | Why it matters | -| --- | --- | -| Generate keys on your own machine | A key generated by the server was known to the server and crossed the network | -| Set a passphrase on every private key | A stolen laptop or a stray backup is then useless to whoever holds it | -| Prefer `ed25519` | Short, fast, and no key-size trap. Use RSA 4096 only where something old refuses `ed25519` | -| One key per person, per device | A shared key cannot be revoked for one person, and logs cannot tell you who connected | -| Build instances with a service account | Access then survives someone leaving. See [Use a Service Account](../use-a-service-account/index.md) | -| Never send a private key anywhere | Not by email, chat, ticket or shared drive. Move the public key instead | -| Set `600` on the key and `700` on `~/.ssh` | OpenSSH refuses to use a private key that others can read | -| Give every key a comment | `name@laptop-2026-08` tells you whose key it is and when it was made | -| Use an agent, never `ssh -A` | Agent forwarding lets the remote host borrow your key. `ProxyJump` does not | -| Rotate on a schedule and after events | See [When to rotate a key](#when-to-rotate-a-key) | - -## Create a protected key - -!!! note - `openstack keypair create KEY_PAIR_NAME` asks the cloud to generate the - key for you and returns the private half over the network, with no - passphrase. Generate the key on your own machine instead, and upload only - the public half. See - [Create and manage keypairs via CLI](create-and-manage-keypairs-via-cli.md) - for the full command reference. - -Generate the pair on your workstation: - -``` { .sh } -ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_ed25519 -``` - -| Option | Meaning | -| --- | --- | -| `-t ed25519` | Key type. Fast, small, and secure at a fixed strength | -| `-a 100` | Rounds used to derive the encryption key from your passphrase, which slows down anyone guessing it | -| `-C` | A comment stored in the key, so you can identify it later | -| `-f` | Where to write the pair | - -You are prompted for a passphrase. Set one. Use a phrase of several unrelated -words that you do not use anywhere else, and store it in your password manager. -The passphrase encrypts the private key on disk, so a copied key file is -worthless without it. - -The command writes two files: - -| File | Contents | Who may see it | -| --- | --- | --- | -| `id_rdc_ed25519` | Private key, encrypted with your passphrase | You only | -| `id_rdc_ed25519.pub` | Public key | Anyone | - -Then fix the permissions: - -``` { .sh } -chmod 700 ~/.ssh -chmod 600 ~/.ssh/id_rdc_ed25519 -``` - -### Add a passphrase later - -To add or change the passphrase on a key you already hold, without changing -the key itself: - -``` { .sh } -ssh-keygen -p -f ~/.ssh/id_rdc_ed25519 -``` - -!!! warning - This protects the file from here on, but the key material does not change. - If the key may already have been copied or exposed, a new passphrase - achieves nothing. Generate a new key and rotate. - -### Check a key fingerprint - -A fingerprint identifies a key without revealing it, and lets you confirm that -the key in the RDC is the one on your laptop: - -``` { .sh } -ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub -``` - -``` { .sh .no-copy } -256 SHA256:tMVbtbRdT2vmaZU2ciC5LLWrbql5yw6X8L+OMujXJ1Y name@laptop-2026-08 (ED25519) -``` - -The RDC displays MD5 fingerprints, so add `-E md5` when comparing against -`openstack keypair list`: - -``` { .sh } -ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub -E md5 -``` - -``` { .sh .no-copy } -256 MD5:57:71:50:18:ac:c2:ed:d9:54:05:c0:62:42:68:1d:3e name@laptop-2026-08 (ED25519) -``` - -## Use an SSH agent - -A passphrase prompt on every connection is the main reason people leave keys -unprotected. An agent solves that: unlock the key once per session, and the -agent holds it in memory. - -Start an agent and add the key: - -``` { .sh } -eval "$(ssh-agent -s)" -ssh-add -t 8h ~/.ssh/id_rdc_ed25519 -``` - -`-t 8h` makes the agent forget the key after eight hours, so an unattended -laptop does not stay unlocked overnight. List what the agent currently holds -with `ssh-add -l`, and drop everything with `ssh-add -D`. - -Add an entry to `~/.ssh/config` so the right key is offered automatically: - -``` { .sh } -Host rdc-* - User ubuntu - IdentityFile ~/.ssh/id_rdc_ed25519 - IdentitiesOnly yes - AddKeysToAgent yes -``` - -| Platform | Notes | -| --- | --- | -| Linux | Most desktops start an agent at login, so `eval` is rarely needed | -| macOS | Use `ssh-add --apple-use-keychain` and add `UseKeychain yes` to the config | -| Windows | Run `Set-Service ssh-agent -StartupType Automatic`, then `Start-Service ssh-agent`, in an elevated PowerShell | - -!!! warning - Do not use agent forwarding (`ssh -A` or `ForwardAgent yes`). Anyone with - root on the machine you connect to can use your forwarded agent to - authenticate as you elsewhere. Use `ProxyJump` instead, which keeps the key - on your workstation: - - ``` { .sh } - ssh -J ubuntu@BASTION_IP ubuntu@NODE_IP - ``` - -## When to rotate a key - -Rotate on a schedule, and immediately on any of the trigger events. - -| Trigger | Urgency | -| --- | --- | -| Routine cycle | Every 12 months | -| Someone leaves the team or project | Same day, see [When someone leaves](../create-and-manage-identity/use-a-service-account.md#when-someone-leaves) | -| A laptop or phone is lost or stolen | Immediately | -| A private key was sent over email or chat | Immediately | -| A key was stored without a passphrase on a shared machine | Immediately | -| An instance shows signs of compromise | Immediately, and see [If a key is compromised](#if-a-key-is-compromised) | -| A key was shared between people | At the next window, then stop sharing | - -## Rotate a key step by step - -The order matters. Add and prove the new key before you remove the old one, so -that a mistake never locks you out. - -!!! note - Before you start, confirm you have a second way in: console access through - the [RDC Dashboard](https://dashboard.cloud.nesi.org.nz/), or a colleague - with their own working key. Recovering an instance whose only key is gone - is slow. - -### 1. Check what you have - -List the keypairs on your account and note the name, type and fingerprint of -the one you are replacing: - -``` { .sh } -openstack keypair list -``` - -``` { .sh .no-copy } -+-----------------+-------------------------------------------------+------+ -| Name | Fingerprint | Type | -+-----------------+-------------------------------------------------+------+ -| name-rdc-2025-03 | f0:4f:53:d1:3d:aa:71:d9:ef:b6:32:22:88:c2:68:42 | ssh | -+-----------------+-------------------------------------------------+------+ -``` - -Then list every instance that was built with it, so you know the full set of -hosts to update: - -``` { .sh } -openstack server list --long -c Name -c "Key Name" -c Networks -``` - -Add anything the RDC cannot see: nodes reached through a bastion, hosts you -added the key to by hand, and any automation that holds a copy. - -### 2. Generate the new key - -Follow [Create a protected key](#create-a-protected-key), giving the new key -its own filename and a comment that dates it: - -``` { .sh } -ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_2026_08 -``` - -Keep the old key in place for now. You need it to install the new one. - -### 3. Add the new key - -Append the new public key to `authorized_keys` on every host from step 1, -authenticating with the old key. The new key is added alongside the old, so -nothing breaks yet: - -``` { .sh } -NEWPUB=$(cat ~/.ssh/id_rdc_2026_08.pub) -ssh -i ~/.ssh/id_rdc_2025_03 ubuntu@INSTANCE_IP \ - "umask 077; mkdir -p ~/.ssh; grep -qxF '$NEWPUB' ~/.ssh/authorized_keys \ - || echo '$NEWPUB' >> ~/.ssh/authorized_keys" -``` - -The `grep -qxF` test makes the command safe to run twice: it appends the key -only if it is not already there. - -Repeat for every host. For more than a handful, see -[Rotate keys on a cluster](#rotate-keys-on-a-cluster). - -### 4. Test the new key - -Leave your current session open. In a **second terminal**, connect with the new -key only: - -``` { .sh } -ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@INSTANCE_IP -``` - -`IdentitiesOnly=yes` stops SSH quietly falling back to the old key and giving -you a false pass. You should be prompted for the new passphrase, then let in. - -Do not continue until this succeeds on **every** host. - -### 5. Remove the old key - -Now revoke the old key where it actually matters. Match on the key body rather -than the comment, which anyone can edit: - -``` { .sh } -OLDPUB=$(awk '{print $2}' ~/.ssh/id_rdc_2025_03.pub) -ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP \ - "grep -v -F '$OLDPUB' ~/.ssh/authorized_keys > ~/.ssh/ak.new \ - && mv ~/.ssh/ak.new ~/.ssh/authorized_keys \ - && chmod 600 ~/.ssh/authorized_keys" -``` - -Confirm what is left, and that you recognise all of it: - -``` { .sh } -ssh -i ~/.ssh/id_rdc_2026_08 ubuntu@INSTANCE_IP "ssh-keygen -lf ~/.ssh/authorized_keys" -``` - -Check other accounts on the host as well, `root` in particular: - -``` { .sh } -sudo ssh-keygen -lf /root/.ssh/authorized_keys -``` - -Delete the old private key from your workstation once every host is done. - -### 6. Update OpenStack - -Import the new public key so that instances built from now on receive it: - -``` { .sh } -openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub name-rdc-2026-08 -openstack keypair delete name-rdc-2025-03 -``` - -A dated name keeps the two keys visible side by side and makes the change -obvious in Terraform or Heat plans. - -!!! note - If the keypair name is pinned in a template you cannot easily edit, reuse - it instead. OpenStack keypairs cannot be updated in place, so delete and - re-create: - - ``` { .sh } - openstack keypair delete KEY_PAIR_NAME - openstack keypair create --public-key ~/.ssh/id_rdc_2026_08.pub KEY_PAIR_NAME - ``` - - Between the two commands the name does not exist, so pause any automation - that builds instances. - -Confirm the fingerprint in the RDC matches the key on your workstation: - -``` { .sh } -openstack keypair list -ssh-keygen -lf ~/.ssh/id_rdc_2026_08.pub -E md5 -``` - -### 7. Record the change - -Note in your team documentation or ticket system: - -- The date, and who performed the rotation -- Old and new key names, and their fingerprints -- Which hosts were updated, and any that were skipped -- Why the key was rotated, if it was not the routine cycle -- The date the next routine rotation is due - -Fingerprints are the useful part: they let anyone confirm later which key a -host actually trusts. - -## Rotate keys on a cluster - -Kubernetes and other multi-node clusters need the same steps, applied to every -node. Choose one of the approaches below rather than editing nodes by hand. - -### Update every node - -Use configuration management so the result is repeatable and easy to audit. The -Ansible `authorized_key` module with `exclusive: true` sets the file to exactly -the keys you list, which handles the add and the remove in one pass: - -{% raw %} - -``` { .yaml } -- name: Set the authorised keys for the ubuntu user - ansible.posix.authorized_key: - user: ubuntu - state: present - exclusive: true - key: "{{ lookup('file', '~/.ssh/id_rdc_2026_08.pub') }}" -``` - -{% endraw %} - -!!! warning - `exclusive: true` removes every other key for that user, including any - added by a platform tool or a colleague. Run it against one node first, and - check the result before you run it against the rest. - -Reach nodes on a private network through the bastion: - -``` { .sh } -ansible-playbook -i inventory.ini rotate-keys.yml \ - --ssh-common-args '-o ProxyJump=ubuntu@BASTION_IP' -``` - -### Replace nodes instead - -Replacing nodes is usually cleaner than editing them. A node built after the -keypair is updated comes up with only the new key, so there is nothing to -remove and no chance of a missed host. - -Drain and replace one node at a time so the workload stays up: - -``` { .sh } -kubectl drain NODE_NAME --ignore-daemonsets --delete-emptydir-data -``` - -Delete the node through your cluster tooling, let the group build a -replacement, then confirm it before moving on: - -``` { .sh } -kubectl get nodes -``` - -!!! warning - `openstack server rebuild --key-name NEW_KEY SERVER` also resets the key, - but it rebuilds the instance from its image and destroys everything on the - root disk. Only use it where the instance holds no state. - -### Watch out for new nodes - -Editing `authorized_keys` on a running node changes that node alone. When the -cluster scales up, or replaces a failed node, the replacement is built from the -keypair recorded in the cluster or node-group definition. - -So update the definition as well as the nodes, or the old key reappears on the -next node that is built. After any scaling event, spot-check a new node: - -``` { .sh } -ssh -i ~/.ssh/id_rdc_2026_08 -o IdentitiesOnly=yes ubuntu@NEW_NODE_IP \ - "ssh-keygen -lf ~/.ssh/authorized_keys" -``` - -## If a key is compromised - -Assume the instances are compromised too, not only the key. Rotate straight -away rather than waiting for a maintenance window, and work through the checks -below on every host the key could reach. - -Look for keys you did not add, a common way of keeping access: - -``` { .sh } -ssh-keygen -lf ~/.ssh/authorized_keys -sudo ssh-keygen -lf /root/.ssh/authorized_keys -``` - -Review which keys have been used to log in, and from where: - -``` { .sh } -sudo journalctl -u ssh --grep "Accepted publickey" -``` - -Each line records the fingerprint that was accepted, so you can tell which -sessions were yours and which were not. On older images that still write a text -log, use `sudo grep "Accepted publickey" /var/log/auth.log` instead. - -Confirm the daemon is not accepting passwords as well: - -``` { .sh } -sudo sshd -T | grep -E "permitrootlogin|passwordauthentication" -``` - -`passwordauthentication` should read `no`. `permitrootlogin` reads -`prohibit-password` on the Ubuntu cloud images, which allows `root` in by key -only. Set it to `no` unless something genuinely needs `root` over SSH. - -Then work through the rest: - -- Rotate the key, following the procedure above -- Rotate any [application credentials](../create-and-manage-identity/index.md) - and EC2 credentials that the instance or the person held -- Rebuild rather than clean any instance you cannot fully account for -- Tighten the [security groups](../create-and-manage-networks/index.md) so that - port 22 is reachable only from the addresses you expect -- Please [contact Support](mailto:support@nesi.org.nz) if data may have been - exposed - -!!! note - An `authorized_keys` entry can be restricted to the addresses it will be - accepted from, which limits the value of a stolen key: - - ``` { .sh .no-copy } - from="10.0.0.0/24",restrict ssh-ed25519 AAAAC3Nz... name@laptop-2026-08 - ``` diff --git a/docs/user-guides/create-and-manage-keypairs/secure-your-ssh-keys.md b/docs/user-guides/create-and-manage-keypairs/secure-your-ssh-keys.md new file mode 100644 index 0000000..49ba110 --- /dev/null +++ b/docs/user-guides/create-and-manage-keypairs/secure-your-ssh-keys.md @@ -0,0 +1,165 @@ +--- +hidden: false +label_names: +- keypairs +- security +- ssh +position: 1 +title: Secure Your SSH Keys +description: Protect an SSH private key with a passphrase, keep it in an agent, and confirm the key in the RDC is the one on your workstation. +--- + +An SSH private key is what gets you in to your instances. Unlike a password it +does not expire, nothing prompts you to change it, and a copy can sit forgotten +on an old laptop for years. + +This page covers generating a key that is protected from the start, and working +with it day to day. For replacing a key that already exists, see +[Rotate SSH Keys](rotate-ssh-keys.md). + +## What makes a key safer + +| Choice | Effect | +| --- | --- | +| Generating the key on your own machine | A key the cloud generates for you was known to the cloud, and crossed the network | +| A passphrase on the private key | A stolen laptop or a stray backup is then useless to whoever holds it | +| `ed25519` over RSA | Short, fast, and no key-size trap. RSA 4096 is there for anything too old to accept `ed25519` | +| One key per person, per device | A shared key cannot be revoked for one person, and logs cannot tell you who connected | +| Instances built with a service account | Access survives someone leaving. See [Use a Service Account](../create-and-manage-identity/use-a-service-account.md) | +| Moving the public key, never the private one | A private key that has been through email, chat, a ticket or a shared drive should be treated as exposed | +| `600` on the key and `700` on `~/.ssh` | OpenSSH refuses to use a private key that others can read | +| A comment on every key | `name@laptop-2026-08` tells you whose key it is and when it was made | +| An agent rather than `ssh -A` | Agent forwarding lets the remote host borrow your key. `ProxyJump` does not | +| Rotation on a routine cycle | See [When to rotate a key](rotate-ssh-keys.md#when-to-rotate-a-key) | + +## Create a protected key + +!!! note + `openstack keypair create KEY_PAIR_NAME` asks the cloud to generate the + key for you and returns the private half over the network, with no + passphrase. Generating the key on your own machine and uploading only the + public half avoids both. See + [Create and manage keypairs via CLI](create-and-manage-keypairs-via-cli.md) + for the full command reference. + +Generate the pair on your workstation: + +``` { .sh } +ssh-keygen -t ed25519 -a 100 -C "name@laptop-2026-08" -f ~/.ssh/id_rdc_ed25519 +``` + +| Option | Meaning | +| --- | --- | +| `-t ed25519` | Key type. Fast, small, and secure at a fixed strength | +| `-a 100` | Rounds used to derive the encryption key from your passphrase, which slows down anyone guessing it | +| `-C` | A comment stored in the key, so you can identify it later | +| `-f` | Where to write the pair | + +You are prompted for a passphrase. The passphrase encrypts the private key on +disk, so a copied key file is worthless without it. A phrase of several +unrelated words, not used anywhere else and kept in your password manager, works +well here. + +The command writes two files: + +| File | Contents | Who may see it | +| --- | --- | --- | +| `id_rdc_ed25519` | Private key, encrypted with your passphrase | You only | +| `id_rdc_ed25519.pub` | Public key | Anyone | + +Then set the permissions: + +``` { .sh } +chmod 700 ~/.ssh +chmod 600 ~/.ssh/id_rdc_ed25519 +``` + +### Add a passphrase later + +To add or change the passphrase on a key you already hold, without changing +the key itself: + +``` { .sh } +ssh-keygen -p -f ~/.ssh/id_rdc_ed25519 +``` + +!!! note + This protects the file from here on, but the key material does not change. + If the key may already have been copied or exposed, a new passphrase + achieves nothing, and a new key is needed instead. See + [Rotate SSH Keys](rotate-ssh-keys.md). + +### Check a key fingerprint + +A fingerprint identifies a key without revealing it, and lets you confirm that +the key in the RDC is the one on your laptop: + +``` { .sh } +ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub +``` + +``` { .sh .no-copy } +256 SHA256:tMVbtbRdT2vmaZU2ciC5LLWrbql5yw6X8L+OMujXJ1Y name@laptop-2026-08 (ED25519) +``` + +The RDC displays MD5 fingerprints, so add `-E md5` when comparing against +`openstack keypair list`: + +``` { .sh } +ssh-keygen -lf ~/.ssh/id_rdc_ed25519.pub -E md5 +``` + +``` { .sh .no-copy } +256 MD5:57:71:50:18:ac:c2:ed:d9:54:05:c0:62:42:68:1d:3e name@laptop-2026-08 (ED25519) +``` + +## Use an SSH agent + +A passphrase prompt on every connection is the main reason people leave keys +unprotected. An agent avoids that: unlock the key once per session, and the +agent holds it in memory. + +Start an agent and add the key: + +``` { .sh } +eval "$(ssh-agent -s)" +ssh-add -t 8h ~/.ssh/id_rdc_ed25519 +``` + +`-t 8h` makes the agent forget the key after eight hours, so an unattended +laptop does not stay unlocked overnight. List what the agent currently holds +with `ssh-add -l`, and drop everything with `ssh-add -D`. + +An entry in `~/.ssh/config` gets the right key offered automatically: + +``` { .sh } +Host rdc-* + User ubuntu + IdentityFile ~/.ssh/id_rdc_ed25519 + IdentitiesOnly yes + AddKeysToAgent yes +``` + +| Platform | Notes | +| --- | --- | +| Linux | Most desktops start an agent at login, so `eval` is rarely needed | +| macOS | Use `ssh-add --apple-use-keychain` and add `UseKeychain yes` to the config | +| Windows | Run `Set-Service ssh-agent -StartupType Automatic`, then `Start-Service ssh-agent`, in an elevated PowerShell | + +!!! note + Agent forwarding (`ssh -A` or `ForwardAgent yes`) lets anyone with root on + the machine you connect to use your forwarded agent to authenticate as you + elsewhere. `ProxyJump` keeps the key on your workstation instead: + + ``` { .sh } + ssh -J ubuntu@BASTION_IP ubuntu@NODE_IP + ``` + +## Retiring a key + +Deleting a keypair in the RDC does **not** revoke access to instances that are +already running, because the key lives in `~/.ssh/authorized_keys` on the +instance once it is built. See +[Rotate SSH Keys](rotate-ssh-keys.md) for what that means in practice, and +[If a Key Is Compromised](if-a-key-is-compromised.md) if a key may already be in +someone else's hands. diff --git a/docs/user-guides/index.md b/docs/user-guides/index.md index 9b3ad5c..1eb413d 100644 --- a/docs/user-guides/index.md +++ b/docs/user-guides/index.md @@ -24,23 +24,17 @@ Our platform's building blocks include: → Get started with our [Networks User Guides](create-and-manage-networks/index.md) -`Service Accounts` -: Use of Service Accounts on your projects versus personnal accounts. - - → Get started with our [Service Account User Guides](use-a-service-account/index.md) - - `Identity` -: Identity management services to create application credentials and control access to projects. +: Identity management services to create application credentials, share a project service account, and control access to projects. → Get started with our [Identity User Guides](create-and-manage-identity/index.md) + → Keep project access when a team member leaves with a [Service Account](create-and-manage-identity/use-a-service-account.md) + `Object Storage` -: Openstack S3 compatiable object storage. +: Openstack S3 compatible object storage. → Get started with our [Object Storage User Guides](create-and-manage-object-storage/index.md) `Application Programming Interface (API)` : All services are programmable via a public API to enable repeatable definition of infrastructure through software code. - - diff --git a/docs/user-guides/use-a-service-account/.pages.yml b/docs/user-guides/use-a-service-account/.pages.yml deleted file mode 100644 index d3f5a12..0000000 --- a/docs/user-guides/use-a-service-account/.pages.yml +++ /dev/null @@ -1 +0,0 @@ -