# Prepare server Takes a freshly provisioned Ubuntu host that you can only reach as the provider's bootstrap account, creates the user you actually want, and configures that account. When it finishes you log in as `{{ user }}`, not as `ubuntu`. This should be the first playbook you run on a new server. The service playbooks take a `-u [remote user]` and expect that account to already exist — this is the playbook that creates it. ## How it works Two plays, because the second one logs in as the user the first one creates. 1. **Bootstrap the new user** — connects as `{{ bootstrap_user }}` (`ubuntu` on Ubuntu cloud images) with `bootstrap.pem`. Creates `{{ user }}`, installs its `authorized_keys`, and grants it passwordless sudo. 2. **Configure the server as the new user** — connects as `{{ user }}` with `{{ user_key }}`. Hardens sshd, installs the user's own SSH config and key, and sets up zsh. Every task after the first play is done as `{{ user }}`, which is the point: the bootstrap account is used once and then never again. ## Layout ``` prepare-server/ ├── ansible.cfg # read from the CWD; points at ./inventory ├── inventory # the hosts, so -i is not needed ├── bootstrap.pem # provider key: gitignored, must be mode 0600 ├── playbook.yml ├── vars/default.yml └── roles/ ├── user/ # play 1 — create the account, install authorized_keys ├── wheel/ # play 1 — wheel group + passwordless sudo ├── common/ # play 2 — sshd hardening, .ssh config, the user's own key └── zsh/ # play 2 — zsh, oh-my-zsh, autosuggestions, syntax highlighting ``` ## Settings | Variable | Purpose | |---|---| | `server` | The host name written into the `.ssh/config` on the server. It refers to a specific server, so it is *not* used for key comments — those use `inventory_hostname`, and the two may legitimately differ. | | `user` | The account to create, and the account every task after the first play runs as. | | `user_key` | Private key on the control node used to log in as `user`. Its `.pub` sibling must exist — the `user` role authorizes it. | | `bootstrap_user` | The provider account that exists on a fresh host. `ubuntu` on Ubuntu cloud images. | | `bootstrap_key` | The provider key that reaches that account: `bootstrap.pem` next to the playbook. | | `terminus_key`, `clair_key`, `anjin_key` | Additional public keys authorized to log in as `user`. | ## Prerequisites - A fresh host reachable as `{{ bootstrap_user }}`, with passwordless sudo — cloud images ship it that way. The playbook escalates immediately, so a bootstrap account that needs a sudo password will not work unattended. - `bootstrap.pem` in this directory, mode `0600`. OpenSSH refuses to use a private key that is group- or world-readable, and fails with *"UNPROTECTED PRIVATE KEY FILE"* rather than anything resembling a permissions hint. The file is gitignored — do not commit it. - `~/.ssh/id_ed25519` and `~/.ssh/id_ed25519.pub` on the control node. ## Running ```command cd prepare-server ansible-playbook playbook.yml ``` The `cd` is load-bearing. Ansible reads `ansible.cfg` from the **current working directory**, not from the playbook's directory, so [ansible.cfg](ansible.cfg) — and the [inventory](inventory) it names — only apply when you run from in here. Running `ansible-playbook prepare-server/playbook.yml` from the repository root ignores both and silently falls back to `/etc/ansible/hosts`, which happens to list `clair` as well, so it looks like it worked. Ansible also has no by-name discovery for inventory files: `inventory` is read because `ansible.cfg` names it, not because of what it is called. Add `-l ` once the inventory holds more than one. `-u` is deliberately not needed: each play sets `remote_user` itself, so the bootstrap account is used for the first play and the new user for the second. ## Why sudo and authorized_keys are set up in the first play Both have to happen before the second play connects, not because they logically belong to bootstrapping: - **The second play logs in as `{{ user }}`**, so that account must already have a working `authorized_keys` entry. That is why the SSH key tasks live in the `user` role rather than in `common`, where they would otherwise belong. - **The second play escalates with `become`**, so `{{ user }}` must already be able to sudo. The `wheel` role writes `/etc/sudoers.d/wheel`, which means it has to run while we still have an account that can escalate — i.e. during the bootstrap play. Left until the second play it fails with *"Missing sudo password"*. ## Notes - The `user` role authorizes `{{ user_key }}.pub`, so the control node can always log back in as `{{ user }}`. If `user_key` is left at its default and `terminus_key` is that same key, the entry is already there and the task is a no-op. - No password is set on `{{ user }}`, which leaves it locked for password logins, and `common` sets `PasswordAuthentication no` in `sshd_config`. The account is reachable by key only. - The `common` role generates an ed25519 key *for* the user on the server. That is the server's outbound identity and is unrelated to `user_key`, which is the control node's key for logging in. - The `user` role deliberately does not set a shell. Doing so would fight the `zsh` role and flip the shell back on every run. - `roles/common/files/config` is a template despite the extension, and appends the `.tanshu.com` suffix to `{{ server }}`. Adjust it for a host outside that domain.