vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Turn a fresh machine into an instance on first boot
04e063ca5d73b8f57f45d7c8bb07b0fdaa91b5b8 · Keenan Nemetz · 3d ago · parent ea21f923
Turn a fresh machine into an instance on first boot A new instance is one package, one configuration, one directory, and one account. Assembling that by hand is where a first deployment goes wrong: the capability the hardened unit needs for ports 80 and 443, the order that makes the package's own init harmless, and the fact that the first instance admin has to be granted on the host. deploy/cloud-init/vole.yaml is that assembly. It stages two configurations, one that lets Vole hold its own certificate and one that leaves TLS to a proxy, then installs the package, writes the configuration, creates the instance admin, and starts the service. Running it again changes nothing. render.sh renders it for providers that take user data without Terraform, using the placeholder list as the contract with the template. The three Terraform configurations create the machine, its firewall, its address, and optionally the DNS record, and hand it that same file. They are root modules: init and apply, three variables, no other file to edit. First boot is not configuration management. apply does not upgrade an instance or touch its data, because users.db migrates in place and cannot be rolled back.
deploy/cloud-init/vole.yaml +136
| @@ -0,0 +1,136 @@ | |||
| 1 | #cloud-config | ||
| 2 | # A Vole instance on a fresh Ubuntu or Debian machine, ready in a few minutes. | ||
| 3 | # | ||
| 4 | # Each placeholder below is a value the caller supplies. Terraform renders this | ||
| 5 | # file with templatefile(); deploy/cloud-init/render.sh renders it with | ||
| 6 | # envsubst(). Both read a placeholder as a dollar sign followed by a name in | ||
| 7 | # braces, so shell variables in the script at the bottom are written without | ||
| 8 | # braces and are left alone. | ||
| 9 | # | ||
| 10 | # What this file does, in order: cloud-init installs git and curl, writes the | ||
| 11 | # configuration and the admin's SSH key to /opt/vole-deploy, then runs | ||
| 12 | # vole-first-boot, which installs the package, puts the configuration in place, | ||
| 13 | # creates the instance admin, and starts the service. Nothing here is specific to | ||
| 14 | # a cloud provider; see docs/deploy/ for which machine to give it to. | ||
| 15 | |||
| 16 | hostname: ${hostname} | ||
| 17 | fqdn: ${hostname} | ||
| 18 | manage_etc_hosts: true | ||
| 19 | |||
| 20 | package_update: true | ||
| 21 | packages: | ||
| 22 | - git | ||
| 23 | - ca-certificates | ||
| 24 | - curl | ||
| 25 | |||
| 26 | write_files: | ||
| 27 | # Two configurations are staged and the boot script installs the one that | ||
| 28 | # ${tls_mode} names: "acme" lets Vole obtain its own certificate, "proxy" | ||
| 29 | # expects a TLS proxy in front. Editing the installed /etc/vole/vole.toml is | ||
| 30 | # the normal way to change either afterwards. | ||
| 31 | - path: /opt/vole-deploy/vole.acme.toml | ||
| 32 | permissions: "0644" | ||
| 33 | content: | | ||
| 34 | # Written by cloud-init. This is the whole configuration; see | ||
| 35 | # docs/configuration.md for every setting. | ||
| 36 | data_dir = "/var/lib/vole/data" | ||
| 37 | |||
| 38 | # Vole obtains and renews its own TLS certificate, so no proxy is needed. | ||
| 39 | # Port 80 answers the certificate challenge and redirects to HTTPS. | ||
| 40 | http_addr = ":80" | ||
| 41 | https_addr = ":443" | ||
| 42 | public_url = "https://${hostname}" | ||
| 43 | acme_email = "${admin_email}" | ||
| 44 | |||
| 45 | # The only write transport: Git over SSH, authenticated by key. | ||
| 46 | ssh_addr = "0.0.0.0:2222" | ||
| 47 | |||
| 48 | instance_name = "${instance_name}" | ||
| 49 | |||
| 50 | - path: /opt/vole-deploy/vole.proxy.toml | ||
| 51 | permissions: "0644" | ||
| 52 | content: | | ||
| 53 | # Written by cloud-init. This is the whole configuration; see | ||
| 54 | # docs/configuration.md for every setting. | ||
| 55 | data_dir = "/var/lib/vole/data" | ||
| 56 | |||
| 57 | # Nothing terminates TLS here. Put a proxy in front that keeps the Host | ||
| 58 | # header and forwards the client address, then uncomment the last line. | ||
| 59 | http_addr = "127.0.0.1:8080" | ||
| 60 | public_url = "https://${hostname}" | ||
| 61 | # git_http_client_ip_header = "X-Forwarded-For" | ||
| 62 | |||
| 63 | # The only write transport: Git over SSH, authenticated by key. | ||
| 64 | ssh_addr = "0.0.0.0:2222" | ||
| 65 | |||
| 66 | instance_name = "${instance_name}" | ||
| 67 | |||
| 68 | # Ports below 1024 need one capability from the hardened unit. Only the ACME | ||
| 69 | # configuration binds one. | ||
| 70 | - path: /opt/vole-deploy/bind.conf | ||
| 71 | permissions: "0644" | ||
| 72 | content: | | ||
| 73 | [Service] | ||
| 74 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE | ||
| 75 | AmbientCapabilities=CAP_NET_BIND_SERVICE | ||
| 76 | |||
| 77 | # The first account, and an instance admin. A public key is not a secret, so | ||
| 78 | # this stays readable; the account keeps its own copy in users.db. | ||
| 79 | - path: /opt/vole-deploy/admin.pub | ||
| 80 | permissions: "0644" | ||
| 81 | content: | | ||
| 82 | ${admin_ssh_key} | ||
| 83 | |||
| 84 | - path: /usr/local/sbin/vole-first-boot | ||
| 85 | permissions: "0755" | ||
| 86 | content: | | ||
| 87 | #!/bin/sh | ||
| 88 | # One-time setup for a Vole instance. cloud-init runs this once, on first | ||
| 89 | # boot. It is safe to run again: the package install is skipped when vole | ||
| 90 | # is already there, and the account is only created if it does not exist. | ||
| 91 | set -eu | ||
| 92 | |||
| 93 | ADMIN='${admin_account}' | ||
| 94 | PACKAGE_URL='${vole_package_url}' | ||
| 95 | TLS_MODE='${tls_mode}' | ||
| 96 | LOG=/var/log/vole-first-boot.log | ||
| 97 | |||
| 98 | exec >>"$LOG" 2>&1 | ||
| 99 | echo "== vole first boot $(date -u +%Y-%m-%dT%H:%M:%SZ) ==" | ||
| 100 | |||
| 101 | # The package creates the service account, /var/lib/vole, and the database | ||
| 102 | # and repository tree the shipped config names. Installing it before the | ||
| 103 | # configuration is written means the shipped conffile is replaced on | ||
| 104 | # purpose, which is the documented way to configure an instance. | ||
| 105 | if ! command -v vole >/dev/null 2>&1; then | ||
| 106 | echo "downloading $PACKAGE_URL" | ||
| 107 | curl -fsSL --retry 5 --retry-delay 2 -o /opt/vole-deploy/vole.deb "$PACKAGE_URL" | ||
| 108 | DEBIAN_FRONTEND=noninteractive apt-get install -y /opt/vole-deploy/vole.deb | ||
| 109 | rm -f /opt/vole-deploy/vole.deb | ||
| 110 | fi | ||
| 111 | |||
| 112 | install -m 0644 "/opt/vole-deploy/vole.$TLS_MODE.toml" /etc/vole/vole.toml | ||
| 113 | if [ "$TLS_MODE" = "acme" ]; then | ||
| 114 | install -d -m 0755 /etc/systemd/system/vole.service.d | ||
| 115 | install -m 0644 /opt/vole-deploy/bind.conf /etc/systemd/system/vole.service.d/bind.conf | ||
| 116 | fi | ||
| 117 | systemctl daemon-reload | ||
| 118 | |||
| 119 | # An account name is permanent, so this creates it once and leaves it alone | ||
| 120 | # on a later run. | ||
| 121 | if su -s /bin/sh vole -c "vole admin --config /etc/vole/vole.toml account show '$ADMIN'" >/dev/null 2>&1; then | ||
| 122 | echo "account $ADMIN already exists" | ||
| 123 | else | ||
| 124 | su -s /bin/sh vole -c "vole admin --config /etc/vole/vole.toml account create '$ADMIN' --key /opt/vole-deploy/admin.pub --role admin" | ||
| 125 | fi | ||
| 126 | |||
| 127 | systemctl enable --now vole | ||
| 128 | echo "vole is running on https://${hostname}" | ||
| 129 | |||
| 130 | runcmd: | ||
| 131 | - /usr/local/sbin/vole-first-boot | ||
| 132 | |||
| 133 | final_message: >- | ||
| 134 | Vole is up. Open https://${hostname} to sign in, or clone | ||
| 135 | ssh://git@${hostname}:2222/ once you have created a repository. Setup output is | ||
| 136 | in /var/log/vole-first-boot.log. | ||