diff --git a/acl/acl.go b/acl/acl.go
index 8f2476616edd24f2f39825d27ef17f00071fc1fd..f8628d216179516a8ef6cd492e593aa653b48928 100644
--- a/acl/acl.go
+++ b/acl/acl.go
@@ -2,6 +2,7 @@ package acl
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"os"
 	"strings"
@@ -14,6 +15,7 @@ type ACLOp int
 const (
 	OpRead = iota
 	OpWrite
+	OpPeer
 )
 
 func (op ACLOp) String() string {
@@ -22,6 +24,8 @@ func (op ACLOp) String() string {
 		return "READ"
 	case OpWrite:
 		return "WRITE"
+	case OpPeer:
+		return "PEER"
 	}
 	return "<ERROR>"
 }
@@ -61,6 +65,18 @@ func (m *Manager) Check(identity string, op ACLOp, path string) bool {
 	return false
 }
 
+// Load ACL rules from a text file. The file should contain one rule
+// per line, either:
+//
+//   <identity> <path> <op(READ|WRITE)>
+//
+// or:
+//
+//   <identity> <op(PEER)>
+//
+// (PEER ACLs have no path). Empty lines and lines starting with a #
+// are ignored.
+//
 func Load(path string) (*Manager, error) {
 	f, err := os.Open(path)
 	if err != nil {
@@ -77,19 +93,11 @@ func Load(path string) (*Manager, error) {
 		if strings.HasPrefix(line, "#") || line == "" {
 			continue
 		}
-		fields := strings.Fields(line)
-		if len(fields) != 3 {
-			return nil, fmt.Errorf("syntax error at %s:%d: not enough fields", path, lineno)
-		}
-		op, err := parseOp(fields[2])
+		e, err := parseEntry(line)
 		if err != nil {
 			return nil, fmt.Errorf("syntax error at %s:%d: %w", path, lineno, err)
 		}
-		acls = append(acls, Entry{
-			Identity: fields[0],
-			Path:     fields[1],
-			Op:       op,
-		})
+		acls = append(acls, e)
 	}
 
 	if err := scanner.Err(); err != nil {
@@ -99,12 +107,48 @@ func Load(path string) (*Manager, error) {
 	return NewManager(acls), nil
 }
 
+func parseEntry(line string) (e Entry, err error) {
+	fields := strings.Fields(line)
+
+	switch len(fields) {
+	case 2:
+		e.Op, err = parseOp(fields[1])
+		if err != nil {
+			return
+		}
+		if e.Op != OpPeer {
+			err = errors.New("non-PEER acl without path")
+			return
+		}
+
+	case 3:
+		e.Path = fields[1]
+		e.Op, err = parseOp(fields[2])
+		if err != nil {
+			return
+		}
+		if e.Op != OpRead && e.Op != OpWrite {
+			err = errors.New("acl op is not 'read' or 'write'")
+			return
+		}
+
+	default:
+		err = errors.New("not enough fields")
+		return
+	}
+
+	e.Identity = fields[0]
+	return
+}
+
 func parseOp(s string) (op ACLOp, err error) {
 	switch strings.ToLower(s) {
 	case "read", "r":
 		op = OpRead
 	case "write", "w":
 		op = OpWrite
+	case "peer":
+		op = OpPeer
 	default:
 		err = fmt.Errorf("unknown op '%s'", s)
 	}
diff --git a/server.go b/server.go
index 8eafdb756f0f49a4426a134a052efc016b9c57fc..5bc912d793248e23a04848005d040f55e5e4bfaf 100644
--- a/server.go
+++ b/server.go
@@ -101,6 +101,10 @@ func (s *Server) Store(ctx context.Context, req *pb.StoreRequest) (*empty.Empty,
 }
 
 func (s *Server) SyncNodes(ctx context.Context, req *pb.SyncNodesRequest) (*pb.SyncNodesResponse, error) {
+	if !s.acl.Check(common.GetAuthIdentity(ctx), acl.OpPeer, "") {
+		return nil, status.Error(codes.PermissionDenied, "unauthorized")
+	}
+
 	s.mx.Lock()
 	diff := s.nodes.TreeDiff(req.Summary, "/")
 	s.mx.Unlock()