Skip to content

Add root-ubuntu.sh: provision an Ubuntu/Debian server from one file - #24

Merged
ralyodio merged 1 commit into
masterfrom
worktree-server-provision
Aug 24, 2026
Merged

Add root-ubuntu.sh: provision an Ubuntu/Debian server from one file#24
ralyodio merged 1 commit into
masterfrom
worktree-server-provision

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Moves the server provisioner out of the private dottemplates repo and into
cli-tools, where it can be curled onto a box that has nothing on it yet. It is
the odd one out here — bash rather than TypeScript, and not linked onto PATH
— because it has to run before Node exists.

curl -fsSL https://raw.githubusercontent.com/profullstack/cli-tools/master/root-ubuntu.sh \
  | bash -s -- --refresh

Three things had to change for it to live in a public repo and work on a
machine that is not one of ours.

Dotfiles are optional and are not in this repository. They cannot be: a
dotfiles tree carries ssh config, known_hosts and sometimes keys. The script
used to die unless it was sitting inside that checkout, which is exactly what
stopped it running anywhere else. It now resolves a source in order —
DOTFILES_DIR, the directory holding the script, DOTFILES_REPO cloned to a
cache, or nothing — and skips only the dotfile stages when there is none.
install_ssh had to be split for this: authorising the account's own key is not
a dotfiles job, and an earlier cut of this returned early without one, so
accounts got created that could not log in.

Nothing identifying anybody is left in it. ACME_EMAIL has no default,
because a made-up address sends a stranger's certificate warnings into a black
hole; certbot gets --register-unsafely-without-email rather than an empty
-m. There is no default ad slot: a slot id is an account, so shipping one
bills every box that ever runs this to whoever owns it.

Configuration is read, not sourced. The environment has to win over the
file, and . assigns unconditionally, so a sourced config would quietly beat
the value someone just put on the command line — and this runs as root, where
sourcing hands a config file the whole machine. It is KEY=value, parsed, with
nothing executed. Not JSON, because the script runs before apt has installed
jq.

Also in here:

  • a POSIX guard that says so in one sentence when the script is run under sh,
    since /bin/sh on Ubuntu is dash and the one-liner would otherwise fail on
    the first [[ with a syntax error naming a line nobody typed
  • an OS check that warns rather than refuses on an unrecognised release
  • --groups, so accounts can be provisioned from a pipe
  • the landing page still recognises the marker from when this lived in the
    dotfiles repo, without which every already-provisioned box would decide its
    own page was hand-edited and never touch it again

Testing

34 new tests (335 total, tsc --noEmit clean). The pure helpers are cut out of
the file with sed and run in a real bash, so what is asserted is what the file
does rather than what it appears to say. The rest guard the invariants that make
it safe to publish: no personal identifiers, no key material, no default slot,
and a config file whose $(…) stays literal text.

Not yet run as root on a real server — worth a throwaway VPS before a box
anybody depends on.

Two changed defaults

On an existing box, a re-run with no /etc/cli-tools/server.conf will turn the
sponsor ad off and issue certificates with no contact address. Both are
deliberate — neither value can ship in a public file — but they are live
behaviour changes. SPONSOR_AD_SLOT and ACME_EMAIL in that config restore
them.

🤖 Generated with Claude Code

Moves the server provisioner out of the private dotfiles repo and into
cli-tools, where it can be curled onto a box that has nothing on it yet.
It is the odd one out here -- bash rather than TypeScript, and not linked
onto PATH -- because it has to run before Node exists.

Three things had to change for it to live in a public repo and work on a
machine that is not one of ours.

Dotfiles are now optional and are not in this repository. They cannot be:
a dotfiles tree carries ssh config, known_hosts and sometimes keys. The
script used to die unless it was sitting inside that checkout, which is
exactly what stopped it running anywhere else. It now resolves a source
in order -- DOTFILES_DIR, the directory holding the script, DOTFILES_REPO
cloned to a cache, or nothing -- and skips only the dotfile stages when
there is none. install_ssh had to be split for this: authorising the
account's own key is not a dotfiles job, and an earlier cut of this
returned early without one, so accounts got created that could not log in.

Nothing identifying anybody is left in it. ACME_EMAIL has no default,
because a made-up address sends a stranger's certificate warnings into a
black hole, and certbot is passed --register-unsafely-without-email
rather than an empty -m. There is no default ad slot: a slot id is an
account, so shipping one bills every box that ever runs this to whoever
owns it. Credentials come from the environment or the config file only.

Configuration is read, not sourced. The environment has to win over the
file, and `.` assigns unconditionally, so a sourced config would quietly
beat the value someone just put on the command line -- and this runs as
root, where sourcing hands a config file the whole machine. It is
KEY=value, parsed, with nothing executed. Not JSON, because the script
runs before apt has installed jq.

Also: a POSIX guard that says so in one sentence when the script is run
under sh, since /bin/sh on Ubuntu is dash and the old one-liner would
have failed on the first [[ with a syntax error naming a line nobody
typed; an OS check that warns rather than refuses; --groups, so accounts
can be provisioned from a pipe; and the landing page still recognises the
marker from when this lived in the dotfiles repo, without which every
already-provisioned box would decide its own page was hand-edited and
never touch it again.

34 tests cover the pure helpers by cutting them out of the file and
running them in a real bash, plus the invariants that make it safe to
publish: no personal identifiers, no key material, no default slot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

11 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 1 | LOW: 5

Severity Rule Location
HIGH sh-remote-script-execution root-ubuntu.sh:122
HIGH sh-remote-script-execution root-ubuntu.sh:2044
HIGH sh-remote-script-execution root-ubuntu.sh:2048
HIGH sh-remote-script-execution root-ubuntu.sh:2101
HIGH sh-remote-script-execution root-ubuntu.sh:3130
MEDIUM redos-nested-quantifier src/domain-free.ts:56
LOW insecure-temp-file test/blog.test.ts:73
LOW insecure-temp-file test/blog.test.ts:74
LOW insecure-temp-file test/credentials.test.ts:43
LOW insecure-temp-file test/credentials.test.ts:44
LOW secret-generic-api-key test/credentials.test.ts:208

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit dae47bc into master Aug 24, 2026
4 of 5 checks passed
@ralyodio
ralyodio deleted the worktree-server-provision branch August 24, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant