summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-08-25 21:23:04 -0500
committerClaude <noreply@anthropic.com>2026-08-25 21:23:04 -0500
commit2a9ca0577ecdbee8b5a4bc8886c112dbc4260eab (patch)
tree8f64cceb0a7831e8be9b7b76bbc9cc9e82e3f11c
parent9d934f49d032694e98f799f042e5eca30de6119c (diff)
new-app: configure jail mail relay and persist the jail's network alias
Two provisioning gaps in new-app: - The jail's dma.conf inherited the base image's shared SMARTHOST/ PORT/AUTHPATH, but never got a per-app MAILNAME - without one, dma can misroute mail for recipients on the jail's own apex domain as local delivery. EnableMail appends a MAILNAME of <app>.skiff.local, which can't collide with any real domain an app sends to or from. - A classic jail's ip4.addr is only a bind restriction, not a route, until something aliases it onto a host interface. jail.conf.tmpl never set jail(8)'s own `interface` parameter, so nothing ever created that alias - confirmed on skiff-freebsd, where the address had twice been made to work via an ad hoc `ifconfig alias` that didn't survive a reboot. Setting `interface` lets jail(8) manage the alias itself, including on every boot.
-rw-r--r--cmd/skiff/main.go6
-rw-r--r--internal/jailmgr/jailmgr.go19
-rw-r--r--internal/layout/layout.go9
-rw-r--r--internal/tmpl/jail.conf.tmpl1
-rw-r--r--internal/tmpl/tmpl.go1
5 files changed, 31 insertions, 5 deletions
diff --git a/cmd/skiff/main.go b/cmd/skiff/main.go
index 7c9d505..8ca1406 100644
--- a/cmd/skiff/main.go
+++ b/cmd/skiff/main.go
@@ -84,6 +84,7 @@ func cmdNewApp(args []string) error {
healthPath := fs.String("health-path", layout.DefaultHealthPath, "health check path")
serverName := fs.String("server-name", "", "nginx server_name (defaults to <name>)")
baseSnapshot := fs.String("base-snapshot", "ready", "snapshot on the base dataset to clone from")
+ iface := fs.String("iface", layout.DefaultJailIface, "host interface the jail's IP is aliased onto")
fs.Parse(args)
if *name == "" || *ip == "" {
@@ -115,9 +116,12 @@ func cmdNewApp(args []string) error {
return fmt.Errorf("initialize env file: %w", err)
}
- if err := jailmgr.WriteConf(cfg, *name, *ip); err != nil {
+ if err := jailmgr.WriteConf(cfg, *name, *ip, *iface); err != nil {
return fmt.Errorf("write jail config: %w", err)
}
+ if err := jailmgr.EnableMail(cfg, *name); err != nil {
+ return fmt.Errorf("configure mail: %w", err)
+ }
if err := appmeta.Write(cfg.MetaPath(*name), appmeta.Meta{
IP: *ip,
diff --git a/internal/jailmgr/jailmgr.go b/internal/jailmgr/jailmgr.go
index b6a5d28..9ddf9b8 100644
--- a/internal/jailmgr/jailmgr.go
+++ b/internal/jailmgr/jailmgr.go
@@ -16,8 +16,8 @@ import (
// WriteConf renders jail.conf.d/<app>.conf and the jail's own rc.d service
// script + rc.conf enable line. Called once, from `skiff new-app`.
-func WriteConf(cfg layout.Config, app, ip string) error {
- conf, err := tmpl.JailConf(tmpl.JailConfData{Name: app, JailRoot: cfg.JailRoot(app), IP: ip})
+func WriteConf(cfg layout.Config, app, ip, iface string) error {
+ conf, err := tmpl.JailConf(tmpl.JailConfData{Name: app, JailRoot: cfg.JailRoot(app), IP: ip, Iface: iface})
if err != nil {
return err
}
@@ -49,6 +49,21 @@ func WriteConf(cfg layout.Config, app, ip string) error {
return nil
}
+// EnableMail appends a MAILNAME line to the jail's dma.conf, inherited from
+// the base image's SMARTHOST/PORT/AUTHPATH/SECURETRANSFER/STARTTLS. Called
+// once, from `skiff new-app`.
+func EnableMail(cfg layout.Config, app string) error {
+ f, err := os.OpenFile(cfg.DMAConfPath(app), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ return fmt.Errorf("open jail dma.conf: %w", err)
+ }
+ defer f.Close()
+ if _, err := fmt.Fprintf(f, "\nMAILNAME %s.skiff.local\n", app); err != nil {
+ return fmt.Errorf("set MAILNAME in jail dma.conf: %w", err)
+ }
+ return nil
+}
+
func Start(ctx context.Context, out io.Writer, app string) error {
return sh.Run(ctx, out, "", "service", "jail", "start", app)
}
diff --git a/internal/layout/layout.go b/internal/layout/layout.go
index 5c2cf30..34edc32 100644
--- a/internal/layout/layout.go
+++ b/internal/layout/layout.go
@@ -11,6 +11,7 @@ const (
DefaultHealthPort = 8080
DefaultHealthPath = "/healthz"
HealthTimeoutSecs = 10
+ DefaultJailIface = "vtnet0"
)
// Config is Skiff's host-wide configuration. Every field has a sane FreeBSD
@@ -70,8 +71,8 @@ func (c Config) PostReceiveHookPath(app string) string {
return filepath.Join(c.RepoPath(app), "hooks", "post-receive")
}
-// RCScriptPath and RCConfPath are paths inside the jail's own chroot, not
-// the host's rc.d/rc.conf.
+// RCScriptPath, RCConfPath, and DMAConfPath are paths inside the jail's own
+// chroot, not the host's rc.d/rc.conf/dma.conf.
func (c Config) RCScriptPath(app string) string {
return filepath.Join(c.JailRoot(app), "usr", "local", "etc", "rc.d", app)
}
@@ -80,6 +81,10 @@ func (c Config) RCConfPath(app string) string {
return filepath.Join(c.JailRoot(app), "etc", "rc.conf")
}
+func (c Config) DMAConfPath(app string) string {
+ return filepath.Join(c.JailRoot(app), "etc", "dma", "dma.conf")
+}
+
// Everything below is Capistrano-style: each deploy gets its own immutable
// releases/<sha> checkout (built in place, inside the jail's own dataset),
// `current` is a symlink to whichever release is live, and data.db/env are
diff --git a/internal/tmpl/jail.conf.tmpl b/internal/tmpl/jail.conf.tmpl
index 446c8c8..c82b3a6 100644
--- a/internal/tmpl/jail.conf.tmpl
+++ b/internal/tmpl/jail.conf.tmpl
@@ -1,6 +1,7 @@
{{.Name}} {
path = "{{.JailRoot}}";
host.hostname = "{{.Name}}.skiff.local";
+ interface = "{{.Iface}}";
ip4.addr = "{{.IP}}";
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go
index 68e917d..12baa70 100644
--- a/internal/tmpl/tmpl.go
+++ b/internal/tmpl/tmpl.go
@@ -37,6 +37,7 @@ type JailConfData struct {
Name string
JailRoot string
IP string
+ Iface string
}
func JailConf(d JailConfData) (string, error) { return Render(jailConf, d) }