summaryrefslogtreecommitdiffstats
path: root/cmd/ship/host/init.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-10 21:29:08 -0800
committerbndw <ben@bdw.to>2026-02-10 21:29:08 -0800
commit47d4b3b6e4d68660e6e1e05fe2e1c0839f86e40e (patch)
treeaf9b3274d2c4ef2bdcdfc1a074c52a52f8d523e3 /cmd/ship/host/init.go
parent86a9dbce8b6c067c7e94bc6ba5a078b7d85eb9ca (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/init.go')
-rw-r--r--cmd/ship/host/init.go42
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.
212git 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")