diff options
| author | bndw <ben@bdw.to> | 2026-02-10 21:29:08 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-10 21:29:08 -0800 |
| commit | 47d4b3b6e4d68660e6e1e05fe2e1c0839f86e40e (patch) | |
| tree | af9b3274d2c4ef2bdcdfc1a074c52a52f8d523e3 /cmd/ship/host | |
| parent | 86a9dbce8b6c067c7e94bc6ba5a078b7d85eb9ca (diff) | |
Harden security: name validation, scoped sudoers, safe.directory
- Add ValidateName() enforcing ^[a-z][a-z0-9-]{0,62}$ on all entry points
- Tighten sudoers to restrict cp sources/destinations and chown targets
- Scope git safe.directory to www-data user only (preserves CVE-2022-24765)
- Add www-data to git group and caddy to www-data group for fcgiwrap
- Fix vanity import template to use orig_uri placeholder
- Restart (not reload) services after group changes
- Add name validation to env subcommands and deploy_cmd
Diffstat (limited to 'cmd/ship/host')
| -rw-r--r-- | cmd/ship/host/init.go | 42 |
1 files changed, 35 insertions, 7 deletions
diff --git a/cmd/ship/host/init.go b/cmd/ship/host/init.go index e1792f5..0ec573c 100644 --- a/cmd/ship/host/init.go +++ b/cmd/ship/host/init.go | |||
| @@ -157,6 +157,13 @@ func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host | |||
| 157 | if _, err := client.RunSudo("apt-get install -y git fcgiwrap"); err != nil { | 157 | if _, err := client.RunSudo("apt-get install -y git fcgiwrap"); err != nil { |
| 158 | return fmt.Errorf("error installing git/fcgiwrap: %w", err) | 158 | return fmt.Errorf("error installing git/fcgiwrap: %w", err) |
| 159 | } | 159 | } |
| 160 | // Allow git-http-backend (runs as www-data) to access repos owned by git. | ||
| 161 | // Scoped to www-data only, not system-wide, to preserve CVE-2022-24765 protection. | ||
| 162 | // www-data's home is /var/www; ensure it can write .gitconfig there. | ||
| 163 | client.RunSudo("chown www-data:www-data /var/www") | ||
| 164 | if _, err := client.RunSudo("sudo -u www-data git config --global --add safe.directory '*'"); err != nil { | ||
| 165 | return fmt.Errorf("error setting git safe.directory: %w", err) | ||
| 166 | } | ||
| 160 | fmt.Println(" git and fcgiwrap installed") | 167 | fmt.Println(" git and fcgiwrap installed") |
| 161 | 168 | ||
| 162 | fmt.Println("-> Creating git user...") | 169 | fmt.Println("-> Creating git user...") |
| @@ -165,6 +172,14 @@ func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host | |||
| 165 | if _, err := client.RunSudo("usermod -aG docker git"); err != nil { | 172 | if _, err := client.RunSudo("usermod -aG docker git"); err != nil { |
| 166 | return fmt.Errorf("error adding git user to docker group: %w", err) | 173 | return fmt.Errorf("error adding git user to docker group: %w", err) |
| 167 | } | 174 | } |
| 175 | // www-data needs to read git repos for git-http-backend | ||
| 176 | if _, err := client.RunSudo("usermod -aG git www-data"); err != nil { | ||
| 177 | return fmt.Errorf("error adding www-data to git group: %w", err) | ||
| 178 | } | ||
| 179 | // caddy needs to connect to fcgiwrap socket (owned by www-data) | ||
| 180 | if _, err := client.RunSudo("usermod -aG www-data caddy"); err != nil { | ||
| 181 | return fmt.Errorf("error adding caddy to www-data group: %w", err) | ||
| 182 | } | ||
| 168 | fmt.Println(" git user created") | 183 | fmt.Println(" git user created") |
| 169 | 184 | ||
| 170 | fmt.Println("-> Copying SSH keys to git user...") | 185 | fmt.Println("-> Copying SSH keys to git user...") |
| @@ -192,7 +207,20 @@ func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host | |||
| 192 | fmt.Println(" /srv/git created") | 207 | fmt.Println(" /srv/git created") |
| 193 | 208 | ||
| 194 | fmt.Println("-> Writing sudoers for git user...") | 209 | fmt.Println("-> Writing sudoers for git user...") |
| 195 | sudoersContent := `git ALL=(ALL) NOPASSWD: /bin/systemctl restart *, /bin/systemctl daemon-reload, /bin/systemctl reload caddy, /bin/systemctl enable *, /bin/cp * /etc/systemd/system/*, /bin/cp * /etc/caddy/sites-enabled/*, /bin/mkdir -p /var/lib/*, /bin/mkdir -p /var/www/*, /bin/chown * | 210 | sudoersContent := `# Ship git-deploy: allow post-receive hooks to install configs and manage services. |
| 211 | # App names are validated to [a-z][a-z0-9-] before reaching this point. | ||
| 212 | git ALL=(ALL) NOPASSWD: \ | ||
| 213 | /bin/systemctl daemon-reload, \ | ||
| 214 | /bin/systemctl reload caddy, \ | ||
| 215 | /bin/systemctl restart [a-z]*, \ | ||
| 216 | /bin/systemctl enable [a-z]*, \ | ||
| 217 | /bin/cp /var/lib/*/src/.ship/service /etc/systemd/system/*.service, \ | ||
| 218 | /bin/cp /var/lib/*/src/.ship/Caddyfile /etc/caddy/sites-enabled/*.caddy, \ | ||
| 219 | /bin/cp /var/www/*/.ship/Caddyfile /etc/caddy/sites-enabled/*.caddy, \ | ||
| 220 | /bin/mkdir -p /var/lib/*, \ | ||
| 221 | /bin/mkdir -p /var/www/*, \ | ||
| 222 | /bin/chown -R git\:git /var/lib/*, \ | ||
| 223 | /bin/chown git\:git /var/www/* | ||
| 196 | ` | 224 | ` |
| 197 | if err := client.WriteSudoFile("/etc/sudoers.d/ship-git", sudoersContent); err != nil { | 225 | if err := client.WriteSudoFile("/etc/sudoers.d/ship-git", sudoersContent); err != nil { |
| 198 | return fmt.Errorf("error writing sudoers: %w", err) | 226 | return fmt.Errorf("error writing sudoers: %w", err) |
| @@ -205,7 +233,7 @@ func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host | |||
| 205 | fmt.Println("-> Writing vanity import template...") | 233 | fmt.Println("-> Writing vanity import template...") |
| 206 | vanityHTML := `<!DOCTYPE html> | 234 | vanityHTML := `<!DOCTYPE html> |
| 207 | <html><head> | 235 | <html><head> |
| 208 | {{$path := trimPrefix "/" .Req.URL.Path}} | 236 | {{$path := trimPrefix "/" (placeholder "http.request.orig_uri.path")}} |
| 209 | {{$parts := splitList "/" $path}} | 237 | {{$parts := splitList "/" $path}} |
| 210 | {{$module := first $parts}} | 238 | {{$module := first $parts}} |
| 211 | <meta name="go-import" content="{{.Host}}/{{$module}} git https://{{.Host}}/{{$module}}.git"> | 239 | <meta name="go-import" content="{{.Host}}/{{$module}} git https://{{.Host}}/{{$module}}.git"> |
| @@ -237,16 +265,16 @@ func setupGitDeploy(client *ssh.Client, baseDomain string, hostState *state.Host | |||
| 237 | if _, err := client.RunSudo("systemctl enable docker fcgiwrap"); err != nil { | 265 | if _, err := client.RunSudo("systemctl enable docker fcgiwrap"); err != nil { |
| 238 | return fmt.Errorf("error enabling services: %w", err) | 266 | return fmt.Errorf("error enabling services: %w", err) |
| 239 | } | 267 | } |
| 240 | if _, err := client.RunSudo("systemctl start docker fcgiwrap"); err != nil { | 268 | if _, err := client.RunSudo("systemctl restart docker fcgiwrap"); err != nil { |
| 241 | return fmt.Errorf("error starting services: %w", err) | 269 | return fmt.Errorf("error starting services: %w", err) |
| 242 | } | 270 | } |
| 243 | fmt.Println(" Docker and fcgiwrap started") | 271 | fmt.Println(" Docker and fcgiwrap started") |
| 244 | 272 | ||
| 245 | fmt.Println("-> Reloading Caddy...") | 273 | fmt.Println("-> Restarting Caddy...") |
| 246 | if _, err := client.RunSudo("systemctl reload caddy"); err != nil { | 274 | if _, err := client.RunSudo("systemctl restart caddy"); err != nil { |
| 247 | return fmt.Errorf("error reloading Caddy: %w", err) | 275 | return fmt.Errorf("error restarting Caddy: %w", err) |
| 248 | } | 276 | } |
| 249 | fmt.Println(" Caddy reloaded") | 277 | fmt.Println(" Caddy restarted") |
| 250 | 278 | ||
| 251 | hostState.GitSetup = true | 279 | hostState.GitSetup = true |
| 252 | fmt.Println(" Git deployment setup complete") | 280 | fmt.Println(" Git deployment setup complete") |
