{"id":28625925,"url":"https://github.com/paraphraser/ssh-certificates","last_synced_at":"2026-03-01T12:31:13.049Z","repository":{"id":292791472,"uuid":"981935740","full_name":"Paraphraser/ssh-certificates","owner":"Paraphraser","description":"generate and deploy SSH certificates for your home network","archived":false,"fork":false,"pushed_at":"2025-06-22T13:44:18.000Z","size":1476,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-07-10T10:49:56.712Z","etag":null,"topics":["ssh","ssh-certificates"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Paraphraser.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-12T06:08:13.000Z","updated_at":"2025-06-22T22:03:07.000Z","dependencies_parsed_at":"2025-05-12T07:28:27.926Z","dependency_job_id":"d2e36956-299e-4d37-a203-5298d73c3147","html_url":"https://github.com/Paraphraser/ssh-certificates","commit_stats":null,"previous_names":["paraphraser/ssh-certificates"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Paraphraser/ssh-certificates","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Paraphraser%2Fssh-certificates","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Paraphraser%2Fssh-certificates/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Paraphraser%2Fssh-certificates/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Paraphraser%2Fssh-certificates/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Paraphraser","download_url":"https://codeload.github.com/Paraphraser/ssh-certificates/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Paraphraser%2Fssh-certificates/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29969243,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T11:43:06.159Z","status":"ssl_error","status_checked_at":"2026-03-01T11:43:03.887Z","response_time":124,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ssh","ssh-certificates"],"created_at":"2025-06-12T08:31:47.036Z","updated_at":"2026-03-01T12:31:13.012Z","avatar_url":"https://github.com/Paraphraser.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Generating SSH certificates\n\n## contents\n\n- [background](#background)\n- [scenario](#scenario)\n- [terminology](#terminology)\n\n\t- [Public Key Algorithms](#aboutAlgorithms)\n\t- [SSH User](#aboutSSHuser)\n\t- [Certificate Authority](#aboutCAs)\n\t- [Certificate](#aboutCerts)\n\n- [on the subject of a Domain](#onDomain)\n- [on the subject of a DNS server](#onDNSserver)\n- [overview of the process](#processSummary)\n- [tutorial](#tutorial)\n\n\t- [create Certificate Authorities](#makeCAsStep)\n\t- [create host certificate](#makeHostsStep)\n\t- [create user certificate](#makeUsersStep)\n\t- [install host certificate](#installHostsStep)\n\t- [install user certificate](#installUsersStep)\n\t- [make configuration record](#installConfigStep)\n\t- [the acid test](#acidTestStep)\n\t- [the next host](#nextHostStep)\n\n- [scripts reference](#scriptsReference)\n\n\t- [make SSH CAs for domain](#makeCAsCommand)\n\t- [make SSH certificate for host](#makeHostCertCommand)\n\t- [make SSH certificate for user](#makeUserCertCommand)\n\t- [install SSH host package](#installHostCommand)\n\t- [install SSH user package](#installUserCommand)\n\t- [make configuration record for SSH user](#makeConfigCommand)\n\n\t\t- [condensed notation](#makeConfigCondensed)\n\n- [about the root user](#aboutRoot)\n- [useful commands](#usefulCommands)\n- [Termius hint](#termiusHint)\n- [the big picture](#theBigPicture)\n\n\u003ca name=\"background\"\u003e\u003c/a\u003e\n## background\n\nThe first time you used SSH to connect to a computer, you probably typed commands like:\n\n``` console\n$ ssh account@host.domain\n```\n\nThat produced a scary-looking warning which security gurus call the \"Trust On First Use\" or \"TOFU\" pattern:\n\n```\nThe authenticity of host 'host.domain' can't be established.\nED25519 key fingerprint is SHA256:VA8GkmADPpDCD2kOZ595uawSa+6oXwHUDRg4FOoWR1o.\nThis key is not known by any other names.\nAre you sure you want to continue connecting (yes/no/[fingerprint])? \n```\n\nYou typed \"yes\" and were prompted for the account password on the target host, after which you were logged-in. The second time you connected to the same host, you didn't get the TOFU warning and, generally, things stayed that way unless something changed.\n\nFor a lot of people, their SSH journeys end at that point. However, if you ever needed password-less login, you would've learned how to use `ssh-keygen` to generate key-pairs and `ssh-copy-id` to move your keys around. You likely also learned how to set up entries in `~/.ssh/config` so that you could reduce your SSH commands to just:\n\n``` console\n$ ssh host\n```\n\nThereafter, each time you added a new host to your home network, you had to revisit that basic process. At some point you might've wondered about alternatives and you may well have seen hints about using \"SSH certificates\". Googling the topic would have led you to tutorials but, if your experience was anything like mine, the whole thing screamed \"complexity\" so you put everything back in its box.\n\nSSH certificates *are* a bit complicated. But complex problems can often be simplified. And that's what the scripts in this repository are intended to do.\n\n\u003ca name=\"scenario\"\u003e\u003c/a\u003e\n## scenario\n\nJemima Chapman Appleseed operates a small home network consisting of a SOHO router provided by her ISP, an iMac and two Raspberry Pis. Jem runs [Pi-hole](https://pi-hole.net) on her `mypi` host and uses it to resolve the domain names shown in [Table\u0026nbsp;1](#table1). Jem's goal is to set up a scheme of SSH certificates so that she can SSH from any host to any other host, password-free.\n\n| \u003ca name=\"table1\"\u003e\u003c/a\u003eTable 1: Hosts on Jem's Home Network |\n|:---------------------------------------------------------:|\n|![Hosts on Jem's Home Network](./images/jemnet-hosts.png)  |\n\n\u003ca name=\"terminology\"\u003e\u003c/a\u003e\n## terminology\n\nGetting a grip on the terminology is well over half the battle so *please* take the time to do that.\n\n\u003ca name=\"aboutAlgorithms\"\u003e\u003c/a\u003e\n### Public Key Algorithms\n\nSSH supports a bunch of Public Key Cryptography algorithms. You can get a complete list by running:\n\n``` console\n$ ssh -Q HostKeyAlgorithms\n```\n\nThe first time you boot a fresh installation of an operating system (eg Linux, macOS) on a computer, SSH generates key-pairs for a subset of its supported algorithms (usually RSA, ECDSA and ED25519) and places the files in `/etc/ssh`. [Figure\u0026nbsp;1](#fig-etc-ssh) shows the typical initial directory structure:\n\n| \u003ca name=\"fig-etc-ssh\"\u003e\u003c/a\u003eFigure 1: Default file structure – /etc/ssh    |\n|:------------------------------------------------------------------------:|\n|![Default file structure – /etc/ssh](./images/etc-ssh-file-structure.png) |\n\nEach file prefixed with `ssh_host_` is a member of a key-pair. The `.pub` extension denotes the public key, the other file being the private key. The *content* of each key-pair is unique to the host on which it was generated.\n\nThe SSH algorithm that is currently recommended by security gurus is \"ssh-ed25519\" and that's what the scripts in this repository use. \u003c!--A--\u003e\u0026#x1F130; marks the host's public key for the ED25519 algorithm. You will see this file used later.\n\n\u003ca name=\"aboutSSHuser\"\u003e\u003c/a\u003e\n### SSH User\n\nPlease study [Table\u0026nbsp;1](#table1) again and focus on the \"Account\" column and note that Jem logs-in to her computers as both `jem` and `pi`. If you think about it, there's a distinction between Jem (the person) and the account names she uses.\n\nThis tutorial uses the term *\"SSH\u0026nbsp;user\"* to represent the **person**. Although there is no reason why we couldn't re-use either \"jem\" or \"pi\" as Jem's *SSH\u0026nbsp;user* name, we are going to use Jem's initials (\"jca\") to help keep this distinction between *person* and *account* clear.\n\n\u003ca name=\"aboutCAs\"\u003e\u003c/a\u003e\n### Certificate Authority\n\nA Certificate Authority (\"CA\") is just an ordinary public and private key-pair where the private key is only ever used to sign certificates. We'll get to \"certificates\" in a moment.\n\nYou need two Certificate Authorities:\n\n* one for your hosts (the \"host\u0026nbsp;CA\"); and\n* one for your users (the \"user\u0026nbsp;CA\").\n\nA certificate authority's **public** key can be used to verify that a **certificate** was signed by the certificate authority's **private** key. In other words:\n\n* verifying a host certificate with the host CA's public key provides **users** with an assurance that the host is the one they intend to reach; while\n* verifying a user certificate with the user CA's public key provides **hosts** with an assurance that the SSH\u0026nbsp;user is authorised to have access to the host.\n\nPlease read that last dot-point again. The assurance being given to the host is about the SSH\u0026nbsp;user (ie the person), not the account which is being used to communicate with the host via the SSH protocol. It's a subtle but important distinction.\n\n\u003ca name=\"aboutCerts\"\u003e\u003c/a\u003e\n### Certificate\n\nA certificate is the result of binding a public key with a list of \"principals\", then signing that pairing with a Certificate Authority's private key. There are two types of certificate:\n\n* \u003ca name=\"hostCert\"\u003e\u003c/a\u003e**host certificate**\n\n\tThe \"principals\" for a host certificate are a list of the network addresses via which the host is reachable. Think of it like this. Given the basic command:\n\n\t``` console\n\t$ ssh account@«address»\n\t```\n\n\tthen `«address»` might be any of the following:\n\n\t* the host's IP address (eg 192.168.132.70)\n\t* the name of the host (eg mac)\n\t* the host's domain name (eg mac.jemnet.home.arpa)\n\t* the host's multicast domain name (eg mac.local).\n\n\tThe \"principals\" are a comma-separated list of those network addressing mechanisms:\n\n\t```\n\t192.168.132.70,mac,mac.jemnet.home.arpa,mac.local\n\t```\n\n\tA host certificate binds the principals with the host's ED25519 public key, then signs the certificate with the private key of your host\u0026nbsp;CA.\n\n\tAny *client* system in possession of *the public key for your host\u0026nbsp;CA* can authenticate the certificate and be certain that the host is what it claims to be.\n\n\tNote the words *\"the public key for your host\u0026nbsp;CA\".* This is not the same as each host's public key. The *single* public key for your host\u0026nbsp;CA authenticates the separate certificates for **all** of your hosts.\n\n* **user certificate**\n\n\tThe scripts begin by generating an ED25519 public and private key-pair for each *SSH\u0026nbsp;user*.\n\n\t\u003e This is unlike the situation with host certificates where the host's ED25519 key-pair (see [Figure\u0026nbsp;1](#fig-etc-ssh)) is generated automatically the first time the host boots up.\n\n\tThe \"principals\" for a user certificate are a comma-separated list of the account names that the *SSH\u0026nbsp;user* uses across all hosts in the domain. From [Table\u0026nbsp;1](#table1) we can determine that that list is:\n\n\t```\n\tjem,pi\n\t```\n\n\tA user certificate binds the principals (the list of account names) with the *SSH\u0026nbsp;user's* ED25519 public key, then signs the certificate with the private key of your user\u0026nbsp;CA.\n\n\tAny *server* system in possession of *the public key for your user\u0026nbsp;CA* can authenticate the certificate and be certain that the user is who he or she claims to be.\n\n\tNote the words *\"the public key for your user\u0026nbsp;CA\".* This is not the same as each user's public key. The *single* public key for your user\u0026nbsp;CA authenticates **all** your *SSH\u0026nbsp;users*.\n\nThis notion of a *single* public key to authenticate *all* certificates signed by the corresponding private key is what makes \"SSH Certificates\" so powerful. If you add a new host to your network and generate a certificate for it, you don't have to visit all the other hosts in your network to tell them about the new certificate. Your other hosts already have your host\u0026nbsp;CA's public key so the certificate for the new host can be authenticated without further ado. The same if you define a new SSH\u0026nbsp;user. All your hosts already have your user\u0026nbsp;CA's public key so they can authenticate the new user certificate.\n\nTo put this another way, using `ssh-keygen` to generate per-user keys and `ssh-copy-id` to move keys around, gives you a full-mesh or O(n\u003csup\u003e2\u003c/sup\u003e) problem: you have to do increasing amounts of work each time you add a new host or user. Conversely, SSH certificates are an O(n) problem.\n\nThe one weakness of this scheme is that if Jem ever decides to invent a new account name for one of her hosts, she is forced to regenerate her user certificate to add that account name to the list of principals, and then deploy the updated certificate into the `~/.ssh` directory on all of her accounts.\n\n\u003ca name=\"onDomain\"\u003e\u003c/a\u003e\n## on the subject of a Domain\n\nYou *will* need a domain but that doesn't mean you have to register and pay for a domain. If you already own a domain, use it. Otherwise, you can either invent a domain or you can adopt the `home.arpa` domain which is reserved by [RFC8375](https://datatracker.ietf.org/doc/html/rfc8375) for exactly this kind of situation.\n\nIf you decide to use `home.arpa`, I'd recommend inventing your own sub-domain such as the one Jemima has chosen:\n\n```\njemnet.home.arpa\n```\n\n\u003ca name=\"onDNSserver\"\u003e\u003c/a\u003e\n## on the subject of a DNS server\n\nAlthough Jemima is running Pi-hole, that doesn't mean that you must have a Domain Name System server. SSH certificates work just as well with IP addresses or hostnames in `/etc/hosts`. You can also enable multicast DNS services on your hosts and rely on the `.local` domain.\n\nThat said, you will have a better user experience with a DNS server than without. PiHole is easy to spin-up and adding a DNS record is a matter of typing each host's fully-qualified domain name plus its IP address, and clicking \u003ckbd\u003eAdd\u003c/kbd\u003e. [Table\u0026nbsp;2](#table2) is a screen shot of Jem's PiHole DNS page.\n\n| \u003ca name=\"table2\"\u003e\u003c/a\u003eTable 2: Jem's Home Network - PiHole DNS  |\n|:--------------------------------------------------------------:|\n|![PiHole DNS for Jem's Home Network](./images/pihole-hosts.png) |\n\n\u003ca name=\"processSummary\"\u003e\u003c/a\u003e\n## overview of the process\n\nSetting up a scheme of personal SSH certificates boils down to:\n\n1. Run a script to create two Certificate Authorities (\"CA\"), one for your hosts (the \"host\u0026nbsp;CA\") and another for your users (the \"user\u0026nbsp;CA\"). You typically run this script exactly once.\n\n2. Run a script for each of your hosts.\n\n\tSSH has already done the work of setting up an ED25519 public and private key-pair for the host. All the script has to do is to create the host certificate and sign it with your host\u0026nbsp;CA.\n\n3. Run a script for each SSH\u0026nbsp;user identity you want to create.\n\n\tInitially, this will just be you but you can also create identities for members of your family if that's what you want to do. The script generates a public and private key-pair for each SSH\u0026nbsp;user, then creates a certificate which is signed by your user\u0026nbsp;CA.\n\n4. Run a script to install the host's certificate on the host for which it was generated.\n\n5. Run a script to install the user's certificate on each host where the user has an account.\n\n6. Run a script to generate a host record for your `~/.ssh/config`. This is actually an optional step but a host record provides the glue that lets you connect to a host just by running the command:\n\n\t``` console\n\t$ ssh host\n\t```\n\nIf you ever used `ssh-keygen` and `ssh-copy-id` to set up password-less login, most of this will sound pretty familiar, and that's a reasonable observation. **But,** once your certificate scheme is set up, adding a new host is a matter of:\n\n1. Run the script to create a host certificate for the new host; and\n2. Run the script to install the certificate on the new host.\n3. Run the script to generate the host record for your `~/.ssh/config`.\n\n\u003ca name=\"tutorial\"\u003e\u003c/a\u003e\n## tutorial\n\nThis section walks you through the process of creating Jem's SSH certificate scheme. I'm going to assume she's doing this work on her iMac but she could also use one of her Raspberry Pis.\n\n\u003ca name=\"makeCAsStep\"\u003e\u003c/a\u003e\n### create Certificate Authorities\n\nTo create your Certificate Authorities, run this command:\n\n``` console\n$ ./make_ssh_CAs_for_domain.sh jemnet.home.arpa\n```\n\nThe expected response is:\n\n```\nCA fingerprints:\n  256 SHA256:NMePktvHy0o70K1p/b0MF6Gguspzhl1SNhS5Hsvkb5A host-CA generated 11-05-2025 (ED25519)\n  256 SHA256:pgIO/SfZ3UYwx5m1B57cIdfO9gwbplyYfXyxMMDcVm4 user-CA generated 11-05-2025 (ED25519)\nCertificates will have the validity period:\n  -1m:+730d\n```\n\nThe file structure this command produces is shown in [Figure\u0026nbsp;2](#fig-ca-dir):\n\n| \u003ca name=\"fig-ca-dir\"\u003e\u003c/a\u003eFigure 2: Certificate Authority Directory Structure |\n|:----------------------------------------------------------------------------:|\n|![Certificate Authority Directory Structure](./images/CA-file-structure.png)  |\n\nThe files \u003c!--C--\u003e\u0026#x1F132; and \u003c!--E--\u003e\u0026#x1F134; are your private keys. Their default permissions are `-rw-------`. Anyone other than you who possesses these keys can sign certificates authorising \"fake\" hosts and users so you should afford these files whatever protection is appropriate to your threat model. For example, you might keep them in an encrypted volume which is air-gapped from the Internet.\n\nTip:\n\n* Aside from superior encryption characteristics, one of the benefits of the ED25519 algorithm is that its keys are relatively short. You can do things like encode your keys in QR codes, print them, and place the printouts in a safe.\n\n\u003ca name=\"makeHostsStep\"\u003e\u003c/a\u003e\n### create host certificate\n\nTo create a host certificate, run this command:\n\n``` console\n$ ./make_ssh_certificate_for_host.sh mypi jemnet.home.arpa 192.168.132.80\n```\n\nThe expected response-pattern is:\n\n```\nTrying to fetch ed25519 public key for mypi from 192.168.132.80 - succeeded\nHost certificate fingerprint:\n  256 SHA256:Tped10trSSEOfbLkUiJTXEfSUy4OzhhGfitu+s6j0dg jemnet.home.arpa/hosts/mypi/ssh_host_ed25519_key.pub (ED25519-CERT)\n```\n\nAs noted earlier in the discussion about [Public Key Algorithms](#aboutAlgorithms) and depicted in [Figure\u0026nbsp;1](#fig-etc-ssh) at \u003c!--A--\u003e\u0026#x1F130;, SSH automatically creates an ED25519 public key for each host. All the script needs to do is to fetch that key from the host, bind it to the principals (the list of addresses via which the host is reachable), and sign the [host certificate](#hostCert) with the private key of your host\u0026nbsp;CA.\n\nNote that for this to work, the host must be reachable at that IP address at the moment when the command is run. If there is some reason why the host can't be reached, the script will explain how you can satisfy the requirement manually.\n\nThe file structure this command produces is shown in [Figure\u0026nbsp;3](#fig-host-dir):\n\n| \u003ca name=\"fig-host-dir\"\u003e\u003c/a\u003eFigure 3: Directory Structure for host \"mypi\" |\n|:------------------------------------------------------------------------:|\n|![Directory Structure for host \"mypi\"](./images/host-file-structure.png)  |\n\nThe host's public key \u003c!--A--\u003e\u0026#x1F130; in [Figure\u0026nbsp;3](#fig-host-dir) is a cached copy of \u003c!--A--\u003e\u0026#x1F130; from [Figure\u0026nbsp;1](#fig-etc-ssh). The script attempts to re-fetch the host's public key each time it runs, only falling back to the cached copy if the fetch fails (eg the host is offline).\n\nRe-running this command always updates the host certificate \u003c!--J--\u003e\u0026#x1F139;. \n\n\u003ca name=\"makeUsersStep\"\u003e\u003c/a\u003e\n### create user certificate\n\nThe script needs to generate a public+private key-pair for each *SSH\u0026nbsp;user*, bind the *SSH\u0026nbsp;user's* public key to the principals (a list of the account names the *SSH\u0026nbsp;user* employs on various hosts), then sign the certificate with the private key of your user\u0026nbsp;CA.\n\nTo create a user certificate, run this command:\n\n``` console\n$ ./make_ssh_certificate_for_user.sh jca jemnet.home.arpa jem pi\n```\n\nThe expected response is:\n\n```\nUser certificate fingerprint:\n  256 SHA256:4rpiJ00RDyw0NDo946ocRdZV2tYh6uE/alsMScb4/+8 key-pair for jca in jemnet.home.arpa generated 11-05-2025 (ED25519-CERT)\n```\n\nThe file structure this command produces is shown in [Figure\u0026nbsp;4](#fig-user-dir):\n\n| \u003ca name=\"fig-user-dir\"\u003e\u003c/a\u003eFigure 4: Directory Structure for *SSH user* \"jca\" |\n|:-----------------------------------------------------------------------------:|\n|![Directory Structure for SSH user \"jca\"](./images/user-file-structure.png)    |\n\nRe-running this command **never** overwrites either the user's private \u003c!--L--\u003e\u0026#x1F13B; or public \u003c!--N--\u003e\u0026#x1F13D; key but it does always update the user's certificate \u003c!--M--\u003e\u0026#x1F13C;.\n\n\u003ca name=\"installHostsStep\"\u003e\u003c/a\u003e\n### install host certificate\n\nGenerating a host certificate creates an installation package \u003c!--H--\u003e\u0026#x1F137; as a by-product. [Figure\u0026nbsp;5](#fig-host-pak) shows how you can display the contents of a host's installation package:\n\n| \u003ca name=\"fig-host-pak\"\u003e\u003c/a\u003eFigure 5: Installation Package for host \"mypi\" |\n|:-------------------------------------------------------------------------:|\n|![Installation Package for host \"mypi\"](./images/host-package.png)         |\n\nTo apply an installation package to a host, you need to copy both the package and the installation script to the target host. One way of doing that is to use `scp`:\n\n``` console\n$ scp ./install_ssh_host_package.sh jemnet.home.arpa/hosts/mypi/mypi_etc-ssh.tar.gz pi@mypi.jemnet.home.arpa:.\n```\n\nIf this is the first time you have used `ssh` or `scp` to reach the host, you will be presented with both the TOFU warning and a password prompt:\n\n```\nThe authenticity of host 'mypi.jemnet.home.arpa (192.168.132.80)' can't be established.\nED25519 key fingerprint is SHA256:Tped10trSSEOfbLkUiJTXEfSUy4OzhhGfitu+s6j0dg.\nThis key is not known by any other names.\nAre you sure you want to continue connecting (yes/no/[fingerprint])? yes\nWarning: Permanently added 'mypi.jemnet.home.arpa' (ED25519) to the list of known hosts.\npi@mypi.jemnet.home.arpa's password: \ninstall_ssh_host_package.sh                                                                                                      100% 3627     3.6MB/s   00:00    \nmypi_etc-ssh.tar.gz                                                                                                           100% 1166     1.8MB/s   00:00    \n```\n\nNext, login to the host:\n\n``` console\n$ ssh pi@mypi.jemnet.home.arpa\npi@mypi.jemnet.home.arpa's password: \n```\n\nNow you can run the installer:\n\n``` console\n$ sudo ./install_ssh_host_package.sh \nFound mypi_etc-ssh.tar.gz - unpacking\nInstalling configuration glue records\nInstalling known hosts record\nInstalling user CA public key\nInstalling host certificate\n'/tmp/install_ssh_host_package.sh.XVLn5/ssh_host_ed25519_jemnet.home.arpa_key-cert.pub' -\u003e '/etc/ssh/ssh_host_ed25519_jemnet.home.arpa_key-cert.pub'\nAsking systemctl to restart ssh service\nDone!\n```\n\nOnce the installer is complete, you can clean up and logout like this:\n\n``` console\n$ rm install_ssh_host_package.sh mypi_etc-ssh.tar.gz\n$ exit\n```\n\nBecause the host certificate has just been installed, you should remove the TOFU record, otherwise it will just get in the way:\n\n``` console\n$ ssh-keygen -R mypi.jemnet.home.arpa\n```\n\n\u003ca name=\"installUsersStep\"\u003e\u003c/a\u003e\n### install user certificate\n\nGenerating a user certificate creates an installation package as a by-product. [Figure\u0026nbsp;6](#fig-user-pak) shows how you can display the contents of a user's installation package:\n\n| \u003ca name=\"fig-user-pak\"\u003e\u003c/a\u003eFigure 6: Installation Package for *SSH user* \"jca\" |\n|:------------------------------------------------------------------------------:|\n|![Installation Package for SSH user \"jca\"](./images/user-package.png)           |\n\nIf you need to install a user package on *another* host, you should use a mechanism like `scp`. The [install host certificate](#installHostsStep) step contains an example.\n\nIn this case, I am going to assume the installation needs to occur on Jem's iMac, which is the same host she used to generate the certificates in the first place. The simplest approach is to just copy the required files to your home directory:\n\n``` console\n$ cp ./install_ssh_user_package.sh jemnet.home.arpa/users/jca/jca_dot-ssh.tar.gz ~\n```\n\nChange to the home directory and run the script:\n\n``` console\n$ cd\n$ ./install_ssh_user_package.sh jca_dot-ssh.tar.gz\n```\n\nThe expected response-pattern is:\n\n```\nFound jca_dot-ssh.tar.gz - unpacking\n/tmp/install_ssh_user_package.sh.zppbO/jca -\u003e /Users/moi/.ssh/jca\n/tmp/install_ssh_user_package.sh.zppbO/jca-cert.pub -\u003e /Users/moi/.ssh/jca-cert.pub\n/tmp/install_ssh_user_package.sh.zppbO/make_ssh_config_for_jca.sh -\u003e /Users/moi/.ssh/make_ssh_config_for_jca.sh\nDone!\n```\n\nOnce the installer is complete, you can clean up:\n\n``` console\n$ rm install_ssh_user_package.sh jca_dot-ssh.tar.gz\n```\n\nPlease don't try to install a user package for the root account. See [about the root user](#aboutRoot).\n\n\u003ca name=\"installConfigStep\"\u003e\u003c/a\u003e\n### make configuration record\n\nRun the following commands:\n\n``` console\n$ cd ~/.ssh\n$ ./make_ssh_config_for_jca.sh pi mypi \u003e\u003econfig\n```\n\nThe arguments are:\n\n* `pi` is the account name on `mypi`; and\n* `mypi` is the host name\n\nThere is no expected response (everything written to `stdout` is appended to `config`).\n\n\u003ca name=\"acidTestStep\"\u003e\u003c/a\u003e\n### the acid test\n\nRun the command:\n\n``` console\n$ ssh mypi\n```\n\nIf you have done everything correctly then a session will open on `mypi` and you will be logged-in without encountering either a TOFU message or a password prompt.\n\n\u003ca name=\"nextHostStep\"\u003e\u003c/a\u003e\n### the next host\n\nJem has another Raspberry Pi named \"test\". What steps are involved?\n\nWell, the Certificate Authorities and SSH\u0026nbsp;User (\"jca\") have already been created and installed so the only things that need to be done are:\n\n1. Create a host certificate for \"test\".\n2. Install the host certificate for \"test\".\n3. Generate a \"config\" record for \"test\".\n\nOn the Mac where the certificates are being generated:\n\n``` console\n$ cd path/to/certificate/directory\n$ ./make_ssh_certificate_for_host.sh test jemnet.home.arpa 192.168.132.81\n$ scp install_ssh_host_package.sh jemnet.home.arpa/hosts/test/test_etc-ssh.tar.gz pi@test.jemnet.home.arpa:.\n$ ssh pi@test.jemnet.home.arpa\n```\n\nOn the Raspberry Pi named \"test\":\n\n``` console\n$ sudo ./install_ssh_host_package.sh\n$ exit\n```\n\nOn the Mac:\n\n``` console\n$ cd ~/.ssh\n$ ./make_ssh_config_for_jca.sh pi test \u003e\u003econfig\n```\n\nThe process of copying the installation files to the Pi probably produced a TOFU prompt so that needs to be cleaned-up:\n\n``` console\n$ ssh-keygen -R test.jemnet.home.arpa\n```\n\nAnd then the acid test:\n\n``` console\n$ ssh test\n```\n\n\u003ca name=\"scriptsReference\"\u003e\u003c/a\u003e\n## scripts reference\n\n\u003ca name=\"makeCAsCommand\"\u003e\u003c/a\u003e\n### make SSH CAs for domain\n\nUsage:\n\n``` console\n$ ./make_ssh_CAs_for_domain.sh «domain» { «days» }\n```\n\nWhere:\n\n* `«domain»` is a required argument such as `jemnet.home.arpa `\n\n* `«days»` is an optional argument which governs the validity period of any certificates signed by either the host or user\u0026nbsp;CA. The default is 730 days. If you pass 0 then your certificates will never expire.\n\nThe first time you run this command, it generates two public and private key-pairs, one pair for your host\u0026nbsp;CA and another for your user\u0026nbsp;CA.\n\nRe-running the command will **never** overwrite your Certificate Authority key-pairs. The only thing you can change by re-running this command is the validity period for host and user certificates generated subsequently.\n\nMost of the time, you will never need to regenerate your certificate authorities. The most common reason why you *might* want to regenerate your certificate authorities is if the private key for either/both your certificate authorities is compromised. Then, the simplest approach is to delete the CA directory and re-run this command. You will also need to regenerate all your host and user certificates so they are signed by the new private keys, and then install the newly-generated keys.\n\n\u003ca name=\"makeHostCertCommand\"\u003e\u003c/a\u003e\n### make SSH certificate for host\n\nUsage:\n\n``` console\n$ ./make_ssh_certificate_for_host.sh «host» «domain» {«ipAddr» {«name» ... } }\n```\n\nWhere:\n\n* `«host»` is a required argument. It is the name of the host for which you are generating the certificate. This should be the all-lower-case form of whatever the host reports when you run the command:\n\n\t``` console\n\t$ hostname -s\n\t```\n\n* `«domain»` is a required argument such as `jemnet.home.arpa`\n\n* `«ipAddr»` is the IPv4 address of «host». This argument is semi-optional. If it is omitted **and** the `dig` command is available, the script constructs a domain name from `«host».«domain»` then queries the Domain Name System to try to resolve that domain name to the host's IP address.\n\n\tThe argument becomes *required* in the following situations:\n\n\t1. If the Domain Name System query fails (eg because you have not defined the domain name); or\n\n\t2. If you want to pass any `«name»` arguments (below).\n\n* `«name»` is zero or more names via which «host» can be reached.\n\n\tThe `«name»` argument can be one or more of any of the following:\n\n\t- IPv4 addresses (eg 192.168.132.80)\n\t- hostnames (eg in `/etc/hosts`)\n\t- fully-qualified domain names (eg `mypi.jemnet.home.arpa`)\n\t- multicast domain names (eg `mypi.local`)\n\n\t\u003e IPv6 addresses may work too but this hasn't been tested.\n\n\tIf you decide to include the host's IP address in its list of principals, please make sure the host always gets the same IP address. In other words, the IP address should either be configured statically (in the host) or set up as a fixed assignment in your DHCP server.\n\n\tIf `«host»` supports multiple network interfaces, don't forget to include those as alternatives. If you don't include the alternatives then SSH will fall back to prompting for a password.\n\n\tIf you omit the `«name»` arguments entirely, the script generates a default list like this:\n\n\t```\n\t«host»,«host».«domain»,«host».local,«ipAddr»\n\t```\n\nIrrespective of whether the list is created from one or more `«name»` parameters or is generated as a set of defaults, the result is written to the host's directory using the filename:\n\n```\n./«domain»/hosts/«host»/host_principals.csv\n```\n\nOnce written, this file can be changed by re-running the script and passing at least four arguments. The 4\u003csup\u003eth\u003c/sup\u003e and subsequent arguments are used to construct a new list which then overwrites `host_principals.csv`. Alternatively, you can edit `host_principals.csv` with a text editor.\n\n\u003e Note that the `.csv` extension is there to remind you that the values should be comma-separated. Try to avoid editing the file in Microsoft Excel or other spreadsheet applications because they tend to make their own decisions about quoting strings, and the presence of quote marks may make your certificate invalid. Domain names must conform with DNS rules: letters (lower-case preferred), digits and hyphens. You can't use underscores.\n\nEach time you run this script for a given host, it will attempt to fetch the host's ED25519 public key from the host. If the fetch is successful, the result is cached locally at the path:\n\n```\n./«domain»/hosts/«host»/ssh_host_ed25519_key.pub\n```\n\nThe only times a host's public keys (ED25519 or otherwise) are likely to change are:\n\n* If you rebuild a host from scratch and give it the same host name. On first boot, SSH will regenerate all of the host's public and private keys. This implicitly invalidates any cached copies of those keys. You can, however, work around this problem by restoring the contents of the `/etc/ssh` directory from a backup.\n* If you explicitly instruct SSH to regenerate a host's keys. You might do that if you know the host's private keys have become compromised. \n\nGiven that context, re-fetching the host's ED25519 public key each time the script runs is probably overkill. However, it increases the likelihood that the cached copy will remain in sync with the the host.\n\nIn any event, the cached copy will only be re-used if a subsequent run of the script is unable to download a fresh copy of the host's ED25519 public key.\n\nThe script terminates if it is unable to fetch the public key (eg the host is down or unreachable) and no copy exists in the local cache. An error message explains what you need to do.\n\nRe-running the command will always update your host certificate. If you change `host_principals.csv` then the revised list will be included in the new host certificate. In general, the most common reason why you will want to re-run the command is to update the certificate's validity period which starts from \"now\" and extends for the number of days you specified when running the [make SSH CAs for domain](#makeCAsCommand) command.\n\nA by-product of running this script is the creation or update of the following file:\n\n```\n./«domain»/hosts/«host»/«host»_etc-ssh.tar.gz\n```\n\nThis compressed tape archive, which is referred to elsewhere as the \"host package\", contains everything required to set up your host certificate on the target `«host»`. It is an assumed input to the [`install_ssh_host_package.sh`](#installHostCommand) command.\n\nSee [Display a certificate](#uc_show_cert) for instructions on how to produce a human-readable interpretation of a host certificate. Here's an example:\n\n``` console\n$ ssh-keygen -L -f jemnet.home.arpa/hosts/mypi/ssh_host_ed25519_jemnet.home.arpa_key-cert.pub\njemnet.home.arpa/hosts/mypi/ssh_host_ed25519_jemnet.home.arpa_key-cert.pub:\n        Type: ssh-ed25519-cert-v01@openssh.com host certificate\n        Public key: ED25519-CERT SHA256:Tped10trSSEOfbLkUiJTXEfSUy4OzhhGfitu+s6j0dg\n        Signing CA: ED25519 SHA256:NMePktvHy0o70K1p/b0MF6Gguspzhl1SNhS5Hsvkb5A (using ssh-ed25519)\n        Key ID: \"mypi\"\n        Serial: 0\n        Valid: from 2025-05-11T10:02:06 to 2027-05-11T10:03:06\n        Principals: \n                mypi\n                mypi.jemnet.home.arpa\n                mypi.local\n                192.168.132.80\n        Critical Options: (none)\n        Extensions: (none)\n```\n\nNote the list of \"Principals\" in the above. The intention behind caching the principals is that you should be able to regenerate all your host certificates quickly by iterating over your host directories like this:\n\n``` console\nDOMAIN=\"jemnet.home.arpa\"\nfor H in \"./$DOMAIN/hosts/\"* ; do\n   SSHHOST=\"$(basename \"$H\")\"\n   ./make_ssh_certificate_for_host.sh \"$SSHHOST\" \"$DOMAIN\"\ndone \n```\n\nEven though the script is invoked **without** `«name»` arguments, it will regenerate each user certificate while preserving your original intention.\n\n\u003ca name=\"makeUserCertCommand\"\u003e\u003c/a\u003e\n### make SSH certificate for user\n\nUsage:\n\n``` console\n$ ./make_ssh_certificate_for_user.sh sshUser domain {account ... }\n```\n\nWhere:\n\n* `sshUser` is a required argument. It is the name associated with the user for whom you are generating the certificate. See also [SSH User](#aboutSSHuser).\n* `domain` is a required argument such as `jemnet.home.arpa`.\n* `account` is an **optional** argument: \n\n\t- omitted, in which case the \"principals\" for the user certificate is either the `sshUser` (if this is the first run) or whatever was saved from the previous run; or\n\t- one or more account names that `«sshUser»` uses to login on various hosts, providing that any list of account names does not include \"root\" (see [about the root user](#aboutRoot)).\n\nKeep in mind that you will need to regenerate (and deploy) your user certificate each time you invent a new `«account»` name.\n\nWhether you supply a list of `«account»` arguments or rely on the default `sshUser`, the decision is cached in the file:\n\n```\n./«domain»/users/«sshUser»/user_principals.csv\n```\n\n\u003e Note that the `.csv` extension is there to remind you that the values should be comma-separated. Try to avoid editing the file in Microsoft Excel or another spreadsheet application because they tend to make their own decisions about quoting strings, and the presence of quote marks may make your certificate invalid. Account names should conform with Unix expectations since time immemorial: a lower-case letter, followed by zero or more lower-case letters, digits, underscores and hyphens.\n\nIf `user_principals.csv` exists when you run this command **without** `«account»` arguments, the list of principals is taken from `user_principals.csv` rather than defaulting to `sshUser`.\n\nThe intention behind caching the account names is that you should be able to regenerate all your user certificates quickly by iterating over your *\"SSH\u0026nbsp;user\"* directories like this:\n\n``` console\nDOMAIN=\"jemnet.home.arpa\"\nfor U in \"./$DOMAIN/users/\"* ; do\n   SSHUSER=\"$(basename \"$U\")\"\n   ./make_ssh_certificate_for_user.sh \"$SSHUSER\" \"$DOMAIN\"\ndone \n```\n\nEven though the script is invoked **without** `«account»` arguments, it will regenerate each user certificate while preserving your original intention.\n\nRe-running the command will always update your user certificate. If you edit the `user_principals.csv` file by hand then the revised list will be included in the new user certificate. In general, the most common reason why you will want to re-run the command is to update the certificate's validity period which starts from \"now\" and extends for the number of days you specified when running the [make SSH CAs for domain](#makeCAsCommand) command.\n\nA by-product of running this script is the creation or update of the following file:\n\n```\n./«domain»/users/«sshUser»/«sshUser»_dot-ssh.tar.gz\n```\n\nThis compressed tape archive, which is referred to elsewhere as the \"user package\", contains everything required to set up your user certificate on any host where you have a login account. It is an assumed input to the [`install_ssh_user_package.sh`](#installUserCommand) command.\n\nSee [Display a certificate](#uc_show_cert) for instructions on how to produce a human-readable interpretation of a user certificate. Here's an example:\n\n``` console\n$ ssh-keygen -L -f jemnet.home.arpa/users/jca/jca-cert.pub \njemnet.home.arpa/users/jca/jca-cert.pub:\n        Type: ssh-ed25519-cert-v01@openssh.com user certificate\n        Public key: ED25519-CERT SHA256:4rpiJ00RDyw0NDo946ocRdZV2tYh6uE/alsMScb4/+8\n        Signing CA: ED25519 SHA256:pgIO/SfZ3UYwx5m1B57cIdfO9gwbplyYfXyxMMDcVm4 (using ssh-ed25519)\n        Key ID: \"jemnet.home.arpa/users/jca/jca\"\n        Serial: 0\n        Valid: from 2025-05-11T10:02:28 to 2027-05-11T10:03:28\n        Principals: \n                jem\n                pi\n        Critical Options: (none)\n        Extensions: \n                permit-X11-forwarding\n                permit-agent-forwarding\n                permit-port-forwarding\n                permit-pty\n                permit-user-rc\n```\n\nNote the `«account»` names in the list of principals.\n\n\u003ca name=\"installHostCommand\"\u003e\u003c/a\u003e\n### install SSH host package\n\nPrerequisites:\n\n1. The host package exists. Typically, this is named for the host. For example, if the host is named `mypi` then the package name will be:\n\n\t```\n\tmypi_etc-ssh.tar.gz\n\t``` \n\nUsage:\n\n``` console\n$ sudo ./install_ssh_host_package.sh { /path/to/package }\n```\n\nIf `/path/to/package` is omitted, the script assumes that the hostname can be discovered from the `hostname` command, the package name constructed from the `hostname`, and that it will be found in the working directory. If there is some reason why that will not work, you can provide the path to the package via the optional argument.\n\nIf the host package is found, the archive is unpacked to temporary storage, the files it contains are moved into place, and the `sshd` daemon is restarted.\n\nThe script contains a `case` statement which handles restarting `sshd` according to Linux and macOS conventions. The script does not handle other platforms but it should be reasonably easy to extend it to do that.\n\nThe script manipulates the files listed below:\n\n* `/etc/ssh/sshd_config.d/900_«domain».conf`\n\n\tContains the following glue records:\n\n\t```\n\tHostCertificate /etc/ssh/ssh_host_ed25519_«domain»_key-cert.pub\n\tTrustedUserCAKeys /etc/ssh/ssh_trusted_user_CA_public_keys\n\t```\n\n\tThe glue records tell SSH which files have been added to `/etc/ssh` and the roles they perform.\n\n\tIf \t`/etc/ssh/sshd_config.d` does not exist then the installer script assumes a legacy version of SSH is running and appends the glue records to:\n\n\t* `/etc/ssh/sshd_config`\n\n* `/etc/ssh/ssh_host_ed25519_«domain»_key-cert.pub`\n\n\tContains the host certificate. Always replaced.\n\n* `/etc/ssh/ssh_known_hosts`\n\n\tContains the \"certificate authority\" record for hosts in the domain.\n\n\tThink of this file as the equivalent of adding the certificate authority record to the `~/.ssh/known_hosts` file for each user on this host.\n\n\tThe certificate authority embeds the public key for your host\u0026nbsp;CA. When a user on *this* machine attempt to open a connection to a host, SSH uses the public key to authenticate the host certificate of the destination host.\n\n* `/etc/ssh/ssh_trusted_user_CA_public_keys`\n\n\tContains the public key for your user\u0026nbsp;CA. SSH use this to authenticate the user-certificates of users who are attempting to open connections to this host.\n\nWith the exception of the host certificate, the files listed above are only edited if one or more lines in the incoming data is not already present in the target file. In other words, to the maximum extent possible, re-running this script is idempotent.\n\nThe host certificate is always replaced. This allows you to re-run [`make_ssh_certificate_for_host.sh`](#makeHostCertCommand) to update the certificate (eg a new expiration date). The new certificate will be installed when you re-run this script.\n\n\u003ca name=\"installUserCommand\"\u003e\u003c/a\u003e\n### install SSH user package\n\nPrerequisites:\n\n1. The user package exists. Typically, this is named for the [SSH User](#aboutSSHuser). For example, if the SSH User is `jca` then the package name will be:\n\n\t```\n\tjca_dot-ssh.tar.gz\n\t```\n\n2. The user is **not** root. See [about the root user](#aboutRoot).\n \nUsage:\n\n``` console\n$ ./install_ssh_user_package.sh /path/to/package\n```\n\nWhere:\n\n* `/path/to/package` is a required argument.\n\nIf the user package is found, the archive is unpacked to temporary storage and the files it contains are moved into place where they take effect the next time you run any of the SSH-related commands.\n\nScript execution is unconditional in the sense that if you re-run [make SSH certificate for user](#makeUserCertCommand) to update the certificate, the new certificate will be installed when you run this script.\n\n\u003ca name=\"makeConfigCommand\"\u003e\u003c/a\u003e\n### make configuration record for SSH user\n\nPrerequisites:\n\n1. The user package has been installed. One component of the package is a script which is named for the [SSH User](#aboutSSHuser). For example, if the SSH User is `jca` then the script name will be:\n\n\t```\n\t~/.ssh/make_ssh_config_for_jca.sh\n\t``` \n\nUsage:\n\n1. Testing:\n\n\t``` console\n\t$ ./make_ssh_config_for_«SSHuser» account target\n\t```\n\n2. Installation:\n\n\t``` console\n\t$ ./make_ssh_config_for_«SSHuser» account target \u003e\u003e~/.ssh/config\n\t```\n\nwhere\n\n* `account` is the account name on `target` that the user wishes to login with; and\n* `target` is the host name that the user wishes to connect to. This is not a domain name and if you try to supply a target name containing dots, they will be removed.\n\nExample test run:\n\n``` console\n$ cd ~/.ssh\n$ ./make_ssh_config_for_jca.sh pi mypi | cat -n\n     1\n     2\thost mypi\n     3\t  hostname %h.jemnet.home.arpa\n     4\t  user pi\n     5\t  IdentitiesOnly yes\n     6\t  IdentityFile ~/.ssh/jca\n     7\n     8\thost mypi.*\n     9\t  hostname %h\n    10\t  user pi\n    11\t  IdentitiesOnly yes\n    12\t  IdentityFile ~/.ssh/jca\n    13\n```\n\n\u003e Piping to `cat -n` adds line numbers.\n\nWhat does that mean? Well:\n\n* if you type the command:\n\n\t``` console\n\t$ ssh mypi\n\t```\n\n\tthen `mypi` matches line 2 so lines 3..6 are applied. The value `mypi` is represented by `%h` so line 3 becomes:\n\n\t```\n\thostname mypi.jemnet.home.arpa\n\t```\n\n\tTogether with the value `pi` in line 4, the original command is treated as if it had been written as:\n\n\t``` console\n\t$ ssh pi@mypi.jemnet.home.arpa\n\t```\n\n\tWhen the client tries to connect, it uses the user's private key `~/.ssh/jca` from line 6 and infers that the associated user certificate will be in the file `~/.ssh/jca-cert.pub`\n\n* if you type a command containing at least one dot, such as:\n\n\t``` console\n\t$ ssh mypi.jemnet.home.arpa\n\t$ ssh mypi.local\n\t```\n\n\tthen that matches line 8 so lines 9..12 are applied. The `%h` in line 9 stands for the entire argument and, together with line 10, the original commands are treated as if they had been written as:\n\n\t``` console\n\t$ ssh pi@mypi.jemnet.home.arpa\n\t$ ssh pi@mypi.local\n\t```\n\n\tThe handling of the user's private key and inference as to where the user's certificate can be found is the same.\n\n\u003ca name=\"makeConfigCondensed\"\u003e\u003c/a\u003e\n### condensed notation\n\nAlthough `make_ssh_config_for_jca.sh` will only generate configuration records for a single host, you do not have to stick with that pattern. Providing everything else about two or more hosts is in common, you can edit `~/.ssh/config` to specify multiple matches on the `host` field. For example, this structure will match all three of Jem's hosts:\n\n```\nhost mac mypi test\n  hostname %h.jemnet.home.arpa\n  user pi\n  IdentitiesOnly yes\n  IdentityFile ~/.ssh/jca\n\nhost mac.* mypi.* test.*\n  hostname %h\n  user pi\n  IdentitiesOnly yes\n  IdentityFile ~/.ssh/jca\n```\n\n\u003ca name=\"aboutRoot\"\u003e\u003c/a\u003e\n## about the root user\n\nHistorically, Unix has always had a root user. Opinions vary on the wisdom of that original design decision but it was a long time ago when things were very different, and it's always easy to be critical when you have 20:20 hindsight. Nevertheless, I'm firmly in the camp which takes the view that you don't exactly *decrease* your attack surface by enabling and routinely using an all-powerful account with a well-known username.\n\nI use a variety of Unix systems. On:\n\n* macOS and Raspberry Pi OS, the root account is disabled by default and the first user is automatically an administrator with the ability to run `sudo`.\n* Debian and Ubuntu, providing you skip the installation step where you are prompted for a root password (which is what I do), the default user you define then has `sudo` privileges and the root account is disabled (ie just like macOS and Raspberry Pi OS).\n\nIf you \"read between the lines\" of the above, you'll see a security philosophy which includes an underlying assumption (which I endorse) that nobody should ever login as root. You will run into this *underlying assumption* and associated error messages if you:\n\n* Try to run [`make_ssh_certificate_for_user.sh`](#makeUserCertCommand) and include `root` in the list of account names; or\n* Try to run [`install_ssh_user_package.sh`](#installUserCommand) either via `sudo` or when you are logged-in as root.\n\nNow, it's your system and your rules but it's my repo and my rules so I don't need to make it easy for you. What I strongly recommend is adding a privileged user and disabling the root account. Once that is in place, you will be able to run commands using `sudo` and, where necessary or convenient, you will still be able to get a root shell using this pattern:\n\n``` console\n$ sudo -s\n# ... run one or more privileged commands here\n# exit\n$\n```\n\nIn words:\n\n1. Get a privileged shell as root. The system prompt changes to `#` to indicate you are running as root.\n2. Run as many commands as you need without needing to prefix each one with `sudo`.\n3. When you're done, type `exit` or press \u003ckbd\u003econtrol\u003c/kbd\u003e+\u003ckbd\u003ed\u003c/kbd\u003e. You are dropped back to the original user and the system prompt changes back to `$` to indicate you are no longer running as root.\n\nHere is the procedure for adding a privileged user account to your system:\n\n1. Connect to your Unix host and login as root.\n\n2. Make sure that `sudo` is installed by running:\n\n\t``` console\n\t# apt update \u0026\u0026 apt install -y sudo\n\t```\n\n\tNotes:\n\n\t* The leading `#` does not indicate a comment. It is the system prompt which reminds you that you are running as root.\n\n\t* If `sudo` is already installed, this command will do nothing.\n\n3. Define the name of the privileged account you want to create:\n\n\t``` console\n\t# ACCOUNT=«name»\n\t```\n\n\tThe rules for a Unix account name are a lower-case letter followed by lower-case letters, digits, hyphens or underscores.\n\n4. Create the account:\n\n\t``` console\n\t# adduser --home /home/$ACCOUNT --shell /bin/bash $ACCOUNT\n\t```\n\n\tYou will be prompted, twice, for a password. You will also be prompted for other information but it is safe to respond to each prompt by pressing \u003ckbd\u003ereturn\u003c/kbd\u003e.\n\n5. Give the account the ability to run `sudo`:\n\n\t``` console\n\t# usermod -G sudo -a $ACCOUNT\n\t# usermod -G adm -a $ACCOUNT\n\t```\n\n6. Test that you can login as that user:\n\n\t``` console\n\t# su - $ACCOUNT\n\t$\n\t```\n\n\tNotes:\n\n\t* Because you are root when you run that command, you will not be prompted for a password for the account.\n\t* The system prompt will change to `$` indicating you are no longer root.\n\n7. Test that the newly-added user can run commands using `sudo`:\n\n\t``` console\n\t$ sudo echo hello\n\t```\n\n\tYou should be prompted for a password. Respond with the password you defined in step 4. This step confirms the validity of the password.\n\n8. Give the account the ability to run `sudo` **without** being prompted for a password:\n\n\t``` console\n\t$ echo \"$USER  ALL=(ALL) NOPASSWD:ALL\" | sudo tee \"/etc/sudoers.d/$USER\"\n\t```\n\n9. Test that you can execute `sudo` commands **without** being prompted for a password:\n\n\t``` console\n\t$ sudo -K\n\t$ sudo echo hello\n\t```\n\n\tNotes:\n\n\t* When you provided a password to authorise your use of `sudo` in step 7, you were implicitly granted the permission to continue to use `sudo` without another password prompt. The implicit permission persists until you logout or a timeout expires, or if you explicitly revoke the permission, which is what `sudo -K` does.\n\n\t* Despite the revocation, the second command should still succeed **without** prompting for a password, because the account was added to the \"sudoers\" list in step 8.\n\n10. Drop back to the original root account:\n\n\t``` console\n\t$ exit\n\t#\n\t```\n\n\tThe system prompt changes back to `#` to indicate that you are running as root.\n\n11. Explicitly deny SSH access by root:\n\n\t``` console\n\t# echo \"PermitRootLogin no\" \u003e/etc/ssh/sshd_config.d/500-no-root-login.conf\n\t# systemctl restart ssh\n\t```\n\n\tNotes:\n\n\t* In theory, the default for `PermitRootLogin` is `prohibit-password` but experience shows this is not always true. It is better to set `no` explicitly.\n\t* If you need to undo this change, delete `500-no-root-login.conf` and restart SSH.\n\t* If you try to SSH as root while access is disabled, the behaviour is as though you had entered an incorrect password. You are never told that access by root is disabled.\n\n12. Lock the root account (optional but recommended):\n\n\t``` console\n\t# passwd --lock root\n\t```\n\n\tLocking the account:\n\n\t* Prevents use of `su -` or `su - root` to become root;\n\t* Does not prevent use of `sudo -s` to get a shell as root;\n\t* Also prevents login via SSH so, technically, you don't need to do step 11 as well.\n\n\tLocking leaves the original root password in place but disables it. To re-enable the root account (which, of necessity, you will be doing from the privileged account you created earlier):\n\n\t``` console\n\t$ sudo passwd --unlock root\n\t```\n\n13. Logout\n\n\t``` console\n\t# exit\n\t```\n\n\u003ca name=\"usefulCommands\"\u003e\u003c/a\u003e\n## useful commands\n\n1. \u003ca name=\"uc_show_cert\"\u003e\u003c/a\u003eDisplay a certificate:\n\n\t``` console\n\t$ ssh-keygen -L -f «path/to/certificate»\n\t```\n\n2. Display public and private key fingerprints:\n\n\t``` console\n\t$ ssh-keygen -l -f «path/to/key»\n\t```\n\n\u003ca name=\"termiusHint\"\u003e\u003c/a\u003e\n## Termius hint\n\nIf you use the excellent [Termius](https://termius.com) application, you can deploy your *SSH\u0026nbsp;user* private key and certificate into the Termius keychain.\n\nHere's a how-to if you are using a macOS host plus Termius running on an iOS device, where Apple's [Universal Clipboard](https://support.apple.com/en-us/102430) is available:\n\n1. In Termius:\n\n\t* Switch to the \"Keychain\" tab.\n\t* Tap \u003ckbd\u003e+\u003c/kbd\u003e and choose \"Paste Key\".\n\t* Tap in the \"Label\" field and give the key a name (eg \"jca\").\n\n2. On your Mac, type the command:\n\n\t``` console\n\t$ pbcopy \u003c~/.ssh/jca\n\t```\n\n\tThat copies the private key to your Mac's clipboard and, via Universal Clipboard, also to your iOS device.\n\n3. In Termius:\n\n\t* Tap in the \"Private Key\" field.\n\t* Tap the \"paste\" icon. That pastes the private key.\n\n4. On your Mac, type the command:\n\n\t``` console\n\t$ pbcopy \u003c~/.ssh/jca-cert.pub\n\t```\n\n\tThat copies the user certificate to your Mac's clipboard and, like the private key, also to your iOS device.\n\n5. In Termius:\n\n\t* Tap in the \"Public Key\" field.\n\t* Tap the \"paste\" icon. That pastes the certificate.\n\n\t\t\u003e Recall that a user's *certificate* contains the user's public key. Pasting a certificate into a \"Public Key\" field creates no ambiguity.\n\n\t* Leave the \"Passphrase\" field empty.\n\t* Tap \u003ckbd\u003eSave\u003c/kbd\u003e.\n\nThe private key and certificate are text files. If you are using a different combination of hardware or operating systems, or if Universal Clipboard isn't working, you can use any method which results in getting the user's private key and certificate onto your Termius device where you can use local copy and paste.\n\nOnce you have provisioned the new key, you can use it to reach your hosts. In Termius:\n\n* Switch to the \"Hosts\" tab.\n* Tap \u003ckbd\u003e+\u003c/kbd\u003e and choose \"New Host\".\n* Tap in the \"Label\" field and give the host a name (eg \"mypi\").\n* Tap in the \"IP or Hostname\" field and enter a fully-qualified domain name or IP address for the host (eg \"mypi.jemnet.home.arpa\").\n* Scroll down to the \"Username\" field, tap **to the left of the \"person\" icon** and enter an account name on the destination host (eg \"pi\").\n* Leave the \"Password\" field empty.\n* Tap the disclosure arrow \u003c!--disclosure off--\u003e\u0026#xFE65; at the right of the \"Key\" field.\n* Tap the name of the key to select it.\n* Tap \u003ckbd\u003eSave\u003c/kbd\u003e.\n\nYou can expect to encounter a TOFU alert the first time you connect to any host. I haven't been able to find a way to provision Termius with the public key of the host CA so I don't think this can be avoided. Tapping \u003ckbd\u003eContinue\u003c/kbd\u003e adds a record to the \"Known Hosts\" tab. Thereafter, connections will be made on the first tap.\n\n\u003ca name=\"theBigPicture\"\u003e\u003c/a\u003e\n## the big picture\n\n[Figure\u0026nbsp;7](#fig-big-pic) is a high-level view of the various steps in setting up a scheme of SSH certificates. The critical elements which establish trust in your certificates are contained in the cross-over in the middle of the diagram:\n\n* These scripts implement step \"4a\" whereby your host CA's public key is placed in `/etc/ssh` on each host. The semantic meaning is that **all** users on this host will trust all the host certificates in your domain.\n\n\tThe alternative step \"4b\" is more appropriate in situations where a user doesn't have write access to `/etc/ssh`. The semantic meaning is **this** user on this host will trust all the host certificates in your domain.\n\n* Step \"6\" is where the user CA's public key is appended to:\n\n\t```\n\t/etc/ssh/ssh_trusted_user_CA_public_keys\n\t```\n\n\tThe semantic meaning is **this** host will trust all the user certificates in your domain.\n\n| \u003ca name=\"fig-big-pic\"\u003e\u003c/a\u003eFigure 7: The Big Picture |\n|:---------------------------------------------------:|\n|![The Big Picture](./images/the-big-picture.png)     |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparaphraser%2Fssh-certificates","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fparaphraser%2Fssh-certificates","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparaphraser%2Fssh-certificates/lists"}