p0: IANA special-purpose deny table, extra deny prefixes
Block 6to4/Teredo/NAT64-local/site-local/benchmarking/reserved ranges from the IANA registries (families, not decoded embeds). Guard.DenyPrefixes / -deny-prefix covers hairpin WAN /32s; home addresses stay on-box, not in git. Control is tcp/tcp4/tcp6 only.
This commit is contained in:
@@ -5,10 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/sift/internal/guard"
|
||||||
"git.wntrmute.dev/kyle/sift/internal/server"
|
"git.wntrmute.dev/kyle/sift/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,10 +25,31 @@ func defaultConfigPath() string {
|
|||||||
return "/etc/sift/config.toml"
|
return "/etc/sift/config.toml"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type denyFlags []netip.Prefix
|
||||||
|
|
||||||
|
func (d *denyFlags) String() string {
|
||||||
|
var b []string
|
||||||
|
for _, p := range *d {
|
||||||
|
b = append(b, p.String())
|
||||||
|
}
|
||||||
|
return strings.Join(b, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *denyFlags) Set(s string) error {
|
||||||
|
p, err := netip.ParsePrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = append(*d, p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
listen := flag.String("listen", "127.0.0.1:8890", "listen address")
|
listen := flag.String("listen", "127.0.0.1:8890", "listen address")
|
||||||
configPath := flag.String("config", defaultConfigPath(), "config file (optional in P0)")
|
configPath := flag.String("config", defaultConfigPath(), "config file (optional in P0)")
|
||||||
showVersion := flag.Bool("version", false, "print version and exit")
|
showVersion := flag.Bool("version", false, "print version and exit")
|
||||||
|
var deny denyFlags
|
||||||
|
flag.Var(&deny, "deny-prefix", "extra CIDR to block (repeatable; home WAN /32s)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
@@ -37,6 +61,10 @@ func main() {
|
|||||||
if _, err := os.Stat(*configPath); err == nil {
|
if _, err := os.Stat(*configPath); err == nil {
|
||||||
log.Info("config file present but unused in P0", "path", *configPath)
|
log.Info("config file present but unused in P0", "path", *configPath)
|
||||||
}
|
}
|
||||||
|
_ = &guard.Guard{DenyPrefixes: deny} // held for fetch in P1; validated at flag parse
|
||||||
|
if n := len(deny); n > 0 {
|
||||||
|
log.Info("deny prefixes loaded", "count", n)
|
||||||
|
}
|
||||||
|
|
||||||
srv := server.New(version)
|
srv := server.New(version)
|
||||||
hs := srv.HTTPServer(*listen)
|
hs := srv.HTTPServer(*listen)
|
||||||
|
|||||||
Vendored
+8
-1
@@ -1,13 +1,20 @@
|
|||||||
# sift configuration. P0 only uses -listen; remaining keys are reserved for P1+.
|
# sift configuration. P0 uses -listen and -deny-prefix; remaining keys are P1+.
|
||||||
# This file is an example: the service does not assume Nix store paths.
|
# This file is an example: the service does not assume Nix store paths.
|
||||||
|
|
||||||
listen = "127.0.0.1:8890"
|
listen = "127.0.0.1:8890"
|
||||||
|
|
||||||
|
# Extra CIDRs blocked by the SSRF guard (pre-check + Control). Home WAN /32s
|
||||||
|
# belong here so a fetch of the public IP cannot hairpin into the LAN.
|
||||||
|
# Set on-box; do not commit residential addresses to git.
|
||||||
|
# deny_prefixes = ["203.0.113.0/24"]
|
||||||
|
|
||||||
# [upstreams]
|
# [upstreams]
|
||||||
# searxng = "http://127.0.0.1:8888"
|
# searxng = "http://127.0.0.1:8888"
|
||||||
# dixie = "http://dixie.scylla-hammerhead.ts.net:11434/v1"
|
# dixie = "http://dixie.scylla-hammerhead.ts.net:11434/v1"
|
||||||
# honcho = "http://rift.scylla-hammerhead.ts.net:8000"
|
# honcho = "http://rift.scylla-hammerhead.ts.net:8000"
|
||||||
|
|
||||||
|
# user_agent = "sift/dev"
|
||||||
|
|
||||||
# [digest]
|
# [digest]
|
||||||
# model = "ornith-1.5-9b-uncensored"
|
# model = "ornith-1.5-9b-uncensored"
|
||||||
# thinking = false
|
# thinking = false
|
||||||
|
|||||||
+35
-32
@@ -30,13 +30,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
cgnat = netip.MustParsePrefix("100.64.0.0/10") // tailnet IPv4 (CGNAT)
|
|
||||||
tailnetULA = netip.MustParsePrefix("fd7a:115c:a1e0::/48") // tailnet IPv6
|
|
||||||
nat64 = netip.MustParsePrefix("64:ff9b::/96")
|
|
||||||
ipv4zero = netip.MustParsePrefix("0.0.0.0/8")
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxRedirects = 5
|
const maxRedirects = 5
|
||||||
|
|
||||||
// LookupIP resolves a hostname to addresses. Tests inject this.
|
// LookupIP resolves a hostname to addresses. Tests inject this.
|
||||||
@@ -45,6 +38,10 @@ type LookupIP func(ctx context.Context, host string) ([]netip.Addr, error)
|
|||||||
// Guard holds the SSRF policy. A zero Guard is ready to use.
|
// Guard holds the SSRF policy. A zero Guard is ready to use.
|
||||||
type Guard struct {
|
type Guard struct {
|
||||||
LookupIP LookupIP
|
LookupIP LookupIP
|
||||||
|
// DenyPrefixes are extra CIDRs blocked at pre-check and at Control
|
||||||
|
// (home WAN /32s that hairpin through the router into LAN services).
|
||||||
|
// Set from config deny_prefixes / -deny-prefix; never from the URL.
|
||||||
|
DenyPrefixes []netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guard) lookup(ctx context.Context, host string) ([]netip.Addr, error) {
|
func (g *Guard) lookup(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||||
@@ -232,14 +229,28 @@ func parseIPv4Part(s string) (uint64, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAddr is the address predicate applied to every resolved record and to
|
// CheckAddr is the default (no extra prefixes) address predicate.
|
||||||
// the literal IP in Dialer.Control.
|
|
||||||
func CheckAddr(addr netip.Addr) error {
|
func CheckAddr(addr netip.Addr) error {
|
||||||
|
var g Guard
|
||||||
|
return g.CheckAddr(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAddr is the address predicate applied to every resolved record and to
|
||||||
|
// the literal IP in Dialer.Control. Extra DenyPrefixes are checked first.
|
||||||
|
func (g *Guard) CheckAddr(addr netip.Addr) error {
|
||||||
if !addr.IsValid() {
|
if !addr.IsValid() {
|
||||||
return blocked("invalid address")
|
return blocked("invalid address")
|
||||||
}
|
}
|
||||||
addr = addr.Unmap()
|
addr = addr.Unmap()
|
||||||
|
|
||||||
|
if g != nil {
|
||||||
|
for _, p := range g.DenyPrefixes {
|
||||||
|
if p.Contains(addr) {
|
||||||
|
return blockedAddr("denied prefix", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case addr.IsUnspecified():
|
case addr.IsUnspecified():
|
||||||
return blockedAddr("unspecified address", addr)
|
return blockedAddr("unspecified address", addr)
|
||||||
@@ -253,33 +264,17 @@ func CheckAddr(addr netip.Addr) error {
|
|||||||
return blockedAddr("link-local address", addr)
|
return blockedAddr("link-local address", addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nat64.Contains(addr) {
|
if err := checkIANA(addr); err != nil {
|
||||||
var b [4]byte
|
return err
|
||||||
raw := addr.As16()
|
|
||||||
copy(b[:], raw[12:])
|
|
||||||
embedded := netip.AddrFrom4(b)
|
|
||||||
if err := CheckAddr(embedded); err != nil {
|
|
||||||
return blockedAddr("NAT64-embedded", embedded)
|
|
||||||
}
|
|
||||||
return blockedAddr("NAT64 prefix", addr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if v4, ok := ipv4Compatible(addr); ok {
|
if v4, ok := ipv4Compatible(addr); ok {
|
||||||
return CheckAddr(v4)
|
return g.CheckAddr(v4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !addr.IsGlobalUnicast() {
|
if !addr.IsGlobalUnicast() {
|
||||||
return blockedAddr("non-global address", addr)
|
return blockedAddr("non-global address", addr)
|
||||||
}
|
}
|
||||||
if addr.Is4() && ipv4zero.Contains(addr) {
|
|
||||||
return blockedAddr("unspecified address", addr)
|
|
||||||
}
|
|
||||||
if addr.Is4() && cgnat.Contains(addr) {
|
|
||||||
return blockedAddr("tailnet address", addr)
|
|
||||||
}
|
|
||||||
if addr.Is6() && tailnetULA.Contains(addr) {
|
|
||||||
return blockedAddr("tailnet address", addr)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +306,7 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if literal.IsValid() {
|
if literal.IsValid() {
|
||||||
if err := CheckAddr(literal); err != nil {
|
if err := g.CheckAddr(literal); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return u, []netip.Addr{literal}, nil
|
return u, []netip.Addr{literal}, nil
|
||||||
@@ -324,7 +319,7 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
|||||||
return nil, nil, blocked("no addresses for " + u.Hostname())
|
return nil, nil, blocked("no addresses for " + u.Hostname())
|
||||||
}
|
}
|
||||||
for _, a := range addrs {
|
for _, a := range addrs {
|
||||||
if err := CheckAddr(a); err != nil {
|
if err := g.CheckAddr(a); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +329,11 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
|||||||
// Control is a net.Dialer Control / ControlContext hook. address is the
|
// Control is a net.Dialer Control / ControlContext hook. address is the
|
||||||
// remote IP:port about to be connect()ed.
|
// remote IP:port about to be connect()ed.
|
||||||
func (g *Guard) Control(network, address string, _ syscall.RawConn) error {
|
func (g *Guard) Control(network, address string, _ syscall.RawConn) error {
|
||||||
|
switch network {
|
||||||
|
case "tcp", "tcp4", "tcp6":
|
||||||
|
default:
|
||||||
|
return blocked("network " + network)
|
||||||
|
}
|
||||||
host, port, err := net.SplitHostPort(address)
|
host, port, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return blocked("dial address " + address)
|
return blocked("dial address " + address)
|
||||||
@@ -345,7 +345,7 @@ func (g *Guard) Control(network, address string, _ syscall.RawConn) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return blocked("dial address not an IP " + host)
|
return blocked("dial address not an IP " + host)
|
||||||
}
|
}
|
||||||
return CheckAddr(addr)
|
return g.CheckAddr(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ControlContext is the context-aware form; policy does not use ctx today.
|
// ControlContext is the context-aware form; policy does not use ctx today.
|
||||||
@@ -389,7 +389,10 @@ func (g *Guard) Transport() *http.Transport {
|
|||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
TLSHandshakeTimeout: 5 * time.Second,
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
DisableCompression: false,
|
// Go gunzips transparently when this is false. P1 size caps MUST
|
||||||
|
// apply to the decompressed resp.Body (io.LimitReader on the body
|
||||||
|
// the client returns); that also covers zip-bombs.
|
||||||
|
DisableCompression: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -539,3 +539,93 @@ func TestDialerType(t *testing.T) {
|
|||||||
t.Fatal("not a Dialer")
|
t.Fatal("not a Dialer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIANASpecialPurpose(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// One address per required prefix from the P0 review.
|
||||||
|
blocked := []string{
|
||||||
|
"2002:a00:1::1", // 6to4 of 10.0.0.1
|
||||||
|
"2002:7f00:1::1", // 6to4 of 127.0.0.1
|
||||||
|
"2001::1", // Teredo 2001::/32 (inside 2001::/23)
|
||||||
|
"64:ff9b:1::a00:1", // RFC 8215 local-use NAT64 embedding 10.0.0.1
|
||||||
|
"fec0::1", // deprecated site-local
|
||||||
|
"198.18.0.1", // benchmarking
|
||||||
|
"198.19.255.255",
|
||||||
|
"192.0.0.1",
|
||||||
|
"240.0.0.1",
|
||||||
|
"255.255.255.255",
|
||||||
|
"100::1",
|
||||||
|
"2001:db8::1",
|
||||||
|
"2001:10::1", // ORCHID
|
||||||
|
"2001:20::1", // ORCHIDv2
|
||||||
|
}
|
||||||
|
for _, s := range blocked {
|
||||||
|
t.Run(s, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := CheckAddr(mustAddr(s))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("CheckAddr(%s) = nil, want blocked", s)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(err.Error(), "blocked:") {
|
||||||
|
t.Fatalf("%s: %v", s, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allowed := []string{"8.8.8.8", "1.1.1.1", "2001:4860:4860::8888"}
|
||||||
|
for _, s := range allowed {
|
||||||
|
if err := CheckAddr(mustAddr(s)); err != nil {
|
||||||
|
t.Fatalf("CheckAddr(%s) = %v, want allow", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguredDenyPrefix(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// 8.8.8.8 is allowed by the IANA table; a configured deny must still block
|
||||||
|
// it (the hairpin/WAN case).
|
||||||
|
g := &Guard{
|
||||||
|
DenyPrefixes: []netip.Prefix{netip.MustParsePrefix("8.8.8.8/32")},
|
||||||
|
LookupIP: func(_ context.Context, host string) ([]netip.Addr, error) {
|
||||||
|
return []netip.Addr{mustAddr("8.8.8.8")}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := CheckAddr(mustAddr("8.8.8.8")); err != nil {
|
||||||
|
t.Fatalf("default CheckAddr allowed-set: %v", err)
|
||||||
|
}
|
||||||
|
if err := g.CheckAddr(mustAddr("8.8.8.8")); err == nil {
|
||||||
|
t.Fatal("configured /32 allowed")
|
||||||
|
}
|
||||||
|
if !strings.Contains(errString(g.CheckAddr(mustAddr("8.8.8.8"))), "denied prefix") {
|
||||||
|
t.Fatalf("want denied prefix, got %v", g.CheckAddr(mustAddr("8.8.8.8")))
|
||||||
|
}
|
||||||
|
if _, _, err := g.CheckURL(context.Background(), "http://hairpin.example/"); err == nil {
|
||||||
|
t.Fatal("CheckURL with injected denied address allowed")
|
||||||
|
}
|
||||||
|
if err := g.Control("tcp", "8.8.8.8:80", nopConn{}); err == nil {
|
||||||
|
t.Fatal("Control allowed configured deny")
|
||||||
|
}
|
||||||
|
if err := g.CheckAddr(mustAddr("1.1.1.1")); err != nil {
|
||||||
|
t.Fatalf("unrelated public: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errString(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestControlRejectsNonTCP(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
g := &Guard{}
|
||||||
|
if err := g.Control("udp", "8.8.8.8:80", nopConn{}); err == nil {
|
||||||
|
t.Fatal("udp allowed")
|
||||||
|
}
|
||||||
|
if err := g.Control("tcp4", "8.8.8.8:80", nopConn{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := g.Control("tcp6", "[2001:4860:4860::8888]:443", nopConn{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package guard
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
|
// IANA special-purpose registries (IPv4 and IPv6). Families that can embed
|
||||||
|
// IPv4 (6to4, Teredo, well-known NAT64, local-use NAT64) are blocked outright
|
||||||
|
// rather than decoded. IsGlobalUnicast remains a final gate in CheckAddr.
|
||||||
|
//
|
||||||
|
// Sources:
|
||||||
|
// https://www.iana.org/assignments/iana-ipv4-special-registry
|
||||||
|
// https://www.iana.org/assignments/iana-ipv6-special-registry
|
||||||
|
var ianaDeny = []struct {
|
||||||
|
prefix netip.Prefix
|
||||||
|
reason string
|
||||||
|
}{
|
||||||
|
// IPv4
|
||||||
|
{p("0.0.0.0/8"), "unspecified address"},
|
||||||
|
{p("10.0.0.0/8"), "private address"},
|
||||||
|
{p("100.64.0.0/10"), "tailnet address"},
|
||||||
|
{p("127.0.0.0/8"), "loopback address"},
|
||||||
|
{p("169.254.0.0/16"), "link-local address"},
|
||||||
|
{p("172.16.0.0/12"), "private address"},
|
||||||
|
{p("192.0.0.0/24"), "special-purpose address"},
|
||||||
|
{p("192.0.2.0/24"), "special-purpose address"},
|
||||||
|
{p("192.31.196.0/24"), "special-purpose address"},
|
||||||
|
{p("192.52.193.0/24"), "special-purpose address"},
|
||||||
|
{p("192.88.99.0/24"), "special-purpose address"},
|
||||||
|
{p("192.168.0.0/16"), "private address"},
|
||||||
|
{p("192.175.48.0/24"), "special-purpose address"},
|
||||||
|
{p("198.18.0.0/15"), "special-purpose address"},
|
||||||
|
{p("198.51.100.0/24"), "special-purpose address"},
|
||||||
|
{p("203.0.113.0/24"), "special-purpose address"},
|
||||||
|
{p("224.0.0.0/4"), "multicast address"},
|
||||||
|
{p("240.0.0.0/4"), "special-purpose address"},
|
||||||
|
{p("255.255.255.255/32"), "special-purpose address"},
|
||||||
|
|
||||||
|
// IPv6
|
||||||
|
{p("::/128"), "unspecified address"},
|
||||||
|
{p("::1/128"), "loopback address"},
|
||||||
|
{p("64:ff9b::/96"), "NAT64 prefix"},
|
||||||
|
{p("64:ff9b:1::/48"), "NAT64 prefix"},
|
||||||
|
{p("100::/64"), "special-purpose address"},
|
||||||
|
{p("2001::/23"), "special-purpose address"}, // IETF protocol: Teredo, ORCHID, ORCHIDv2, …
|
||||||
|
{p("2001:db8::/32"), "special-purpose address"},
|
||||||
|
{p("2002::/16"), "special-purpose address"}, // 6to4; do not decode embedded v4
|
||||||
|
{p("3fff::/20"), "special-purpose address"}, // documentation (RFC 9637)
|
||||||
|
{p("fc00::/7"), "private address"},
|
||||||
|
{p("fe80::/10"), "link-local address"},
|
||||||
|
{p("fec0::/10"), "special-purpose address"}, // deprecated site-local
|
||||||
|
{p("ff00::/8"), "multicast address"},
|
||||||
|
{p("fd7a:115c:a1e0::/48"), "tailnet address"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func p(s string) netip.Prefix { return netip.MustParsePrefix(s) }
|
||||||
|
|
||||||
|
func checkIANA(addr netip.Addr) error {
|
||||||
|
for _, e := range ianaDeny {
|
||||||
|
if e.prefix.Contains(addr) {
|
||||||
|
return blockedAddr(e.reason, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+12
-1
@@ -30,6 +30,16 @@ in
|
|||||||
description = "HTTPS port for tailscale serve (443 is SearXNG on orion).";
|
description = "HTTPS port for tailscale serve (443 is SearXNG on orion).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
denyPrefixes = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Extra CIDRs the SSRF guard blocks at pre-check and connect time.
|
||||||
|
Put home WAN /32s here (hairpin into LAN). Set on the host; do not
|
||||||
|
commit residential IPs to the sift repo.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
type = settingsFormat.type;
|
type = settingsFormat.type;
|
||||||
default = { };
|
default = { };
|
||||||
@@ -89,7 +99,8 @@ in
|
|||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = "sift";
|
User = "sift";
|
||||||
Group = "sift";
|
Group = "sift";
|
||||||
ExecStart = "${lib.getExe cfg.package} -listen ${cfg.listen} -config /etc/sift/config.toml";
|
ExecStart = "${lib.getExe cfg.package} -listen ${cfg.listen} -config /etc/sift/config.toml"
|
||||||
|
+ lib.concatMapStrings (p: " -deny-prefix ${lib.escapeShellArg p}") cfg.denyPrefixes;
|
||||||
WorkingDirectory = "/var/lib/sift";
|
WorkingDirectory = "/var/lib/sift";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
|
|||||||
Reference in New Issue
Block a user