diff --git a/config/config.go b/config/config.go index c17240f..dbc0aad 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "github.com/zrepl/yaml-config" "io/ioutil" + "log/syslog" "os" "reflect" "regexp" @@ -38,7 +39,7 @@ func (j JobEnum) Name() string { case *PullJob: name = v.Name case *SourceJob: name = v.Name default: - panic(fmt.Sprintf("unknownn job type %T", v)) + panic(fmt.Sprintf("unknown job type %T", v)) } return name } @@ -289,6 +290,7 @@ type StdoutLoggingOutlet struct { type SyslogLoggingOutlet struct { LoggingOutletCommon `yaml:",inline"` + Facility *SyslogFacility `yaml:"facility,optional,fromdefaults"` RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"` } @@ -315,6 +317,14 @@ type PrometheusMonitoring struct { Listen string `yaml:"listen"` } +type SyslogFacility syslog.Priority + +func (f *SyslogFacility) SetDefault() { + *f = SyslogFacility(syslog.LOG_LOCAL0) +} + +var _ yaml.Defaulter = (*SyslogFacility)(nil) + type GlobalControl struct { SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"` } @@ -420,6 +430,40 @@ func (t *MonitoringEnum) UnmarshalYAML(u func(interface{}, bool) error) (err err return } +func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err error) { + var s string + if err := u(&s, true); err != nil { + return err + } + var level syslog.Priority + switch s { + case "kern": level = syslog.LOG_KERN + case "user": level = syslog.LOG_USER + case "mail": level = syslog.LOG_MAIL + case "daemon": level = syslog.LOG_DAEMON + case "auth": level = syslog.LOG_AUTH + case "syslog": level = syslog.LOG_SYSLOG + case "lpr": level = syslog.LOG_LPR + case "news": level = syslog.LOG_NEWS + case "uucp": level = syslog.LOG_UUCP + case "cron": level = syslog.LOG_CRON + case "authpriv": level = syslog.LOG_AUTHPRIV + case "ftp": level = syslog.LOG_FTP + case "local0": level = syslog.LOG_LOCAL0 + case "local1": level = syslog.LOG_LOCAL1 + case "local2": level = syslog.LOG_LOCAL2 + case "local3": level = syslog.LOG_LOCAL3 + case "local4": level = syslog.LOG_LOCAL4 + case "local5": level = syslog.LOG_LOCAL5 + case "local6": level = syslog.LOG_LOCAL6 + case "local7": level = syslog.LOG_LOCAL7 + default: + return fmt.Errorf("invalid syslog level: %q", s) + } + *t = SyslogFacility(level) + return nil +} + var ConfigFileDefaultLocations = []string{ "/etc/zrepl/zrepl.yml", "/usr/local/etc/zrepl/zrepl.yml", diff --git a/config/config_global_test.go b/config/config_global_test.go index e14b936..51204b0 100644 --- a/config/config_global_test.go +++ b/config/config_global_test.go @@ -1,9 +1,11 @@ package config import ( + "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zrepl/yaml-config" + "log/syslog" "testing" ) @@ -72,6 +74,36 @@ global: assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen) } +func TestSyslogLoggingOutletFacility(t *testing.T) { + type SyslogFacilityPriority struct { + Facility string + Priority syslog.Priority + } + syslogFacilitiesPriorities := []SyslogFacilityPriority{ + {"", syslog.LOG_LOCAL0}, // default + {"kern", syslog.LOG_KERN}, {"daemon", syslog.LOG_DAEMON}, {"auth", syslog.LOG_AUTH}, + {"syslog", syslog.LOG_SYSLOG}, {"lpr", syslog.LOG_LPR}, {"news", syslog.LOG_NEWS}, + {"uucp", syslog.LOG_UUCP}, {"cron", syslog.LOG_CRON}, {"authpriv", syslog.LOG_AUTHPRIV}, + {"ftp", syslog.LOG_FTP}, {"local0", syslog.LOG_LOCAL0}, {"local1", syslog.LOG_LOCAL1}, + {"local2", syslog.LOG_LOCAL2}, {"local3", syslog.LOG_LOCAL3}, {"local4", syslog.LOG_LOCAL4}, + {"local5", syslog.LOG_LOCAL5}, {"local6", syslog.LOG_LOCAL6}, {"local7", syslog.LOG_LOCAL7}, + } + + for _, sFP := range syslogFacilitiesPriorities { + logcfg := fmt.Sprintf(` +global: + logging: + - type: syslog + level: info + format: human + facility: %s +`, sFP.Facility) + conf := testValidGlobalSection(t, logcfg) + assert.Equal(t, 1, len(*conf.Global.Logging)) + assert.True(t, SyslogFacility(sFP.Priority) == *(*conf.Global.Logging)[0].Ret.(*SyslogLoggingOutlet).Facility) + } +} + func TestLoggingOutletEnumList_SetDefaults(t *testing.T) { e := &LoggingOutletEnumList{} var i yaml.Defaulter = e diff --git a/daemon/logging/build_logging.go b/daemon/logging/build_logging.go index 8fb3718..52b7e15 100644 --- a/daemon/logging/build_logging.go +++ b/daemon/logging/build_logging.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "log/syslog" "os" "github.com/mattn/go-isatty" @@ -222,6 +223,7 @@ func parseSyslogOutlet(in *config.SyslogLoggingOutlet, formatter EntryFormatter) out = &SyslogOutlet{} out.Formatter = formatter out.Formatter.SetMetadataFlags(MetadataNone) + out.Facility = syslog.Priority(*in.Facility) out.RetryInterval = in.RetryInterval return out, nil } diff --git a/daemon/logging/logging_outlets.go b/daemon/logging/logging_outlets.go index 5a00d42..b03d008 100644 --- a/daemon/logging/logging_outlets.go +++ b/daemon/logging/logging_outlets.go @@ -124,6 +124,7 @@ func (h *TCPOutlet) WriteEntry(e logger.Entry) error { type SyslogOutlet struct { Formatter EntryFormatter RetryInterval time.Duration + Facility syslog.Priority writer *syslog.Writer lastConnectAttempt time.Time } @@ -142,7 +143,7 @@ func (o *SyslogOutlet) WriteEntry(entry logger.Entry) error { if now.Sub(o.lastConnectAttempt) < o.RetryInterval { return nil // not an error toward logger } - o.writer, err = syslog.New(syslog.LOG_LOCAL0, "zrepl") + o.writer, err = syslog.New(o.Facility, "zrepl") o.lastConnectAttempt = time.Now() if err != nil { o.writer = nil diff --git a/docs/configuration/logging.rst b/docs/configuration/logging.rst index f1a8466..a9077bb 100644 --- a/docs/configuration/logging.rst +++ b/docs/configuration/logging.rst @@ -147,6 +147,8 @@ Can only be specified once. - minimum :ref:`log level ` * - ``format`` - output :ref:`format ` + * - ``facility`` + - Which syslog facility to use (default = ``local0``) * - ``retry_interval`` - Interval between reconnection attempts to syslog (default = 0) diff --git a/docs/configuration/transports.rst b/docs/configuration/transports.rst index 0187775..075e918 100644 --- a/docs/configuration/transports.rst +++ b/docs/configuration/transports.rst @@ -76,6 +76,7 @@ Connect The ``tls`` transport uses TCP + TLS with client authentication using client certificates. The client identity is the common name (CN) presented in the client certificate. + It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA `_. For a simple 2-machine setup, see the :ref:`instructions below`. @@ -85,6 +86,10 @@ Since Go binaries are statically linked, you or your distribution need to recomp All file paths are resolved relative to the zrepl daemon's working directory. Specify absolute paths if you are unsure what directory that is (or find out from your init system). +If intermediate CAs are used, the **full chain** must be present in either in the ``ca`` file or the individual ``cert`` files. +Regardless, the client's certificate must be first in the ``cert`` file, with each following certificate directly certifying the one preceding it (see `TLS's specification `_). +This is the common default when using a CA management tool. + Serve ~~~~~ @@ -96,9 +101,9 @@ Serve serve: type: tls listen: ":8888" - ca: /etc/zrepl/ca.crt - cert: /etc/zrepl/prod.crt - key: /etc/zrepl/prod.key + ca: /etc/zrepl/ca.crt + cert: /etc/zrepl/prod.fullchain + key: /etc/zrepl/prod.key client_cns: - "laptop1" - "homeserver" @@ -116,8 +121,8 @@ Connect connect: type: tls address: "server1.foo.bar:8888" - ca: /etc/zrepl/ca.crt - cert: /etc/zrepl/backupserver.crt + ca: /etc/zrepl/ca.crt + cert: /etc/zrepl/backupserver.fullchain key: /etc/zrepl/backupserver.key server_cn: "server1" dial_timeout: # optional, default 10s diff --git a/tlsconf/tlsconf.go b/tlsconf/tlsconf.go index 07a6669..b1cb554 100644 --- a/tlsconf/tlsconf.go +++ b/tlsconf/tlsconf.go @@ -83,8 +83,8 @@ func (l *ClientAuthListener) Accept() (tcpConn *net.TCPConn, tlsConn *tls.Conn, tlsConn.SetDeadline(time.Time{}) peerCerts = tlsConn.ConnectionState().PeerCertificates - if len(peerCerts) != 1 { - err = errors.New("unexpected number of certificates presented by TLS client") + if len(peerCerts) < 1 { + err = errors.New("client must present full RFC5246:7.4.2 TLS client certificate chain") goto CloseAndErr } cn = peerCerts[0].Subject.CommonName