From d3a5f039c14a545012d6e0e87278cfc511f08ec4 Mon Sep 17 00:00:00 2001 From: brent s Date: Wed, 7 Oct 2020 11:25:56 -0400 Subject: [PATCH] checking in some config stuff --- config/TODO | 1 + config/const.go | 96 ++++++++++++++++++++---- config/struct.go | 128 ++++++++++++++++++++++++-------- config/type.go | 14 ++++ config/validator.go | 177 ++++++++++++++++++++++++++++++++++++++------ 5 files changed, 349 insertions(+), 67 deletions(-) create mode 100644 config/TODO diff --git a/config/TODO b/config/TODO new file mode 100644 index 0000000..0cbaa2a --- /dev/null +++ b/config/TODO @@ -0,0 +1 @@ +add default config as struct \ No newline at end of file diff --git a/config/const.go b/config/const.go index 2bf528c..bc39eb9 100644 --- a/config/const.go +++ b/config/const.go @@ -38,17 +38,20 @@ const ( var ( // sshdModify are values we modify. sshdModify = [...]string{ + "AuthenticationMethods", + "ChallengeResponseAuthentication", + "Ciphers", "HostKey", - "PermitRootLogin", - "StrictModes", - "PubkeyAuthentication", + "HostKeyAlgorithms", + "KexAlgorithms", + "MACs", "PasswordAuthentication", "PermitEmptyPasswords", - "ChallengeResponseAuthentication", - "KexAlgorithms", + "PermitRootLogin", + "PrintLastLog", "Protocol", - "Ciphers", - "MACs", + "PubkeyAuthentication", + "StrictModes", } // sshModify are values we modify. @@ -62,6 +65,10 @@ var ( "", } + sshMulti = [...]string{ + "IdentityFile", + } + // authMethods are authentication methods that openssh supports. authMethods = []string{ "any", @@ -92,11 +99,13 @@ var ( // forwardAllows are shared values used by forwarding access control. forwardAllows = []string{"yes", "all", "no", "local", "remote"} - // hostkeyTypes are algorithms/types used for host keys. + // keyTypes are algorithms/types used for host keys and client pubkeys. // The following should generate the same list. // ssh -Q HostKeyAlgorithms | sed -re 's/^/"/g' -e 's/$/",/g' // ssh -Q HostbasedAcceptedKeyTypes | sed -re 's/^/"/g' -e 's/$/",/g' - hostkeyTypes = []string{ + // ssh -Q PubkeyAcceptedKeyTypes | sed -re 's/^/"/g' -e 's/$/",/g' + // ssh -Q HostbasedKeyTypes | sed -re 's/^/"/g' -e 's/$/",/g' + keyTypes = []string{ "ssh-ed25519", "ssh-ed25519-cert-v01@openssh.com", "sk-ssh-ed25519@openssh.com", @@ -196,6 +205,21 @@ var ( "DEBUG3", } + // logFacilities is a list of valid syslog facilities. + logFacilities = []string{ + "DAEMON", + "USER", + "AUTH", // default + "LOCAL0", + "LOCAL1", + "LOCAL2", + "LOCAL3", + "LOCAL4", + "LOCAL5", + "LOCAL6", + "LOCAL7", + } + // macAlgos is a list of valid MAC (Message Authentication Code) values. "-etm" algos are recommended by upstream. // ssh -Q mac | sed -re 's/^/"/g' -e 's/$/",/g' macAlgos = []string{ @@ -224,6 +248,7 @@ var ( // Valid keys for sshdMatchCriteria are tracked via field names in SshdMatchRule. // Multiple criteria can be specified by e.g. "Match User foo, Host bar.tld" sshdMatchCriteria = []string{ + "Address", "All", "User", "Group", @@ -231,18 +256,37 @@ var ( "LocalAddress", "LocalPort", "RDomain", - "Address", + } + + // sshMatchCriteria is a list of valid criteria that can be used in a Match or Host block. + // Valid keys for sshMatchCriteria are tracked via field names in SshMatchRule. + // Multiple criteria can be specified by e.g. "Match User foo, Host bar.tld" + sshMatchCriteria = []string{ + "all", // Must appear alone, or immediately after canonical/final. + "canonical", + "exec", // Requires second argument. + "final", + "host", // Requires second argument. + "localuser", // Requires second argument. + "originalhost", // Requires second argument. + "user", // Requires second argument. } ) // The following are validator maps. var ( // These directives can also begin with "+", "-", or "^", so they need to be stripped off. - // TODO: How to do this non-destructively? + // TODO: How to do this non-destructively and cleanly? + // Currently we just save the struct field's first item with the prefix and strip it in a local var for validating. sshdStripPre = []string{ "HostbasedAcceptedKeyTypes", "KexAlgorithms", } + sshStripPre = []string{ + "HostbasedKeyTypes", + "KexAlgorithms", + } + // validSshdSingleVals are values that can accept a single string value from a static list. validSshdSingleVals = map[string][]string{ "AddressFamily": {"any", "inet", "inet6"}, @@ -251,8 +295,13 @@ var ( "Compression": {"yes", "delayed", "no"}, // "delayed" is legacy, same as "yes". "FingerprintHash": {"sha256", "md5"}, "GatewayPorts": {"yes", "no", "clientspecified"}, + "HostbasedAcceptedKeyTypes": keyTypes, "IgnoreRHosts": {"yes", "shosts-only", "no"}, "LogLevel": logLevels, + "PermitRootLogin": {"yes", "prohibit-password", "forced-commands-only", "no"}, // "without-password" is deprecated. + "PermitTunnel": {"yes", "point-to-point", "ethernet", "no"}, + "PubkeyAuthOptions": {"none", "touch-required"}, + "SyslogFacility": logFacilities, } // validSshdMultiVals are values that can accept multiple values from a static list. @@ -260,9 +309,28 @@ var ( "AuthenticationMethods": authMethods, "CASignatureAlgorithms": sigAlgos, "Ciphers": ciphers, - "HostbasedAcceptedKeyTypes": hostkeyTypes, // NOTE: Can also begin with "+", "-", or "^" - "HostKeyAlgorithms": hostkeyTypes, - "KexAlgorithms": kexAlgos, // NOTE: Can also begin with "+", "-", or "^" + "HostbasedAcceptedKeyTypes": keyTypes, + "HostKeyAlgorithms": keyTypes, + "KexAlgorithms": kexAlgos, "MACs": macAlgos, + "PubkeyAcceptedKeyTypes": keyTypes, + } + + // validSshSingleVals are values that can accept a single string value from a static list. + validSshSingleVals = map[string][]string{ + "AddKeysToAgent": {"yes", "ask", "confirm", "no"}, + "AddressFamily": {"any", "inet", "inet6"}, + "ControlMaster": {"ask", "yes", "no", "autoask", "auto"}, + "FingerprintHash": {"sha256", "md5"}, + } + + // validSshMultiVals are values that can accept multiple values from a static list. + validSshMultiVals = map[string][]string{ + "CASignatureAlgorithms": sigAlgos, + "Ciphers": ciphers, + "HostbasedKeyTypes": keyTypes, + "HostKeyAlgorithms": keyTypes, + "KbdInteractiveDevices": {"bsdauth", "pam"}, + "KexAlgorithms": kexAlgos, } ) diff --git a/config/struct.go b/config/struct.go index b90c006..4dc9ecc 100644 --- a/config/struct.go +++ b/config/struct.go @@ -25,7 +25,7 @@ package config *: These values are not in the upstream config but are allowed via the man page (sshd_config(5) and ssh_config(5)). */ -// More or less a subset of SshdConf. These are valid keywords for Match blocks in sshd_config. +// SshdMatchRule is more or less a subset of SshdConf. These are valid keywords for Match blocks in sshd_config. type SshdMatchRule struct { AcceptEnv []string // * AllowAgentForwarding sshBool // . @@ -49,33 +49,33 @@ type SshdMatchRule struct { ForceCommand string // * GatewayPorts string // . GSSAPIAuthentication sshBool // . - HostbasedAcceptedKeyTypes []string // * + HostbasedAcceptedKeyTypes []string // *+ HostbasedAuthentication sshBool // . HostbasedUsesNameFromPacketOnly sshBool // * IgnoreRhosts string // . // Do we handle includes? Or just let sshd -T handle it? Include string // * // Accepts one or two. If two, first is interactive and second is non-interactive. - IPQoS [2]string // * - KbdInteractiveAuthentication sshBool // * - KerberosAuthentication sshBool // . - LogLevel string // . - MaxAuthTries int // . - MaxSessions int // . - PasswordAuthentication sshBool // .+ - PermitEmptyPasswords sshBool // + - PermitListen string // * - PermitOpen string // * - PermitRootLogin string // .+ - PermitTTY sshBool // . - PermitTunnel string // . - PermitUserRC sshBool // * - PubkeyAcceptedKeyTypes []string // * - PubkeyAuthentication sshBool // .+ - RekeyLimit string // . - RevokedKeys string // * - RDomain string // * - SetEnv map[string]string // * + IPQoS [2]string // * + KbdInteractiveAuthentication sshBool // * + KerberosAuthentication sshBool // . + LogLevel string // . + MaxAuthTries int // . + MaxSessions int // . + PasswordAuthentication sshBool // .+ + PermitEmptyPasswords sshBool // + + PermitListen string // * + PermitOpen string // * + PermitRootLogin string // .+ + PermitTTY sshBool // . + PermitTunnel string // . + PermitUserRC sshBool // * + PubkeyAcceptedKeyTypes []string // * + PubkeyAuthentication sshBool // .+ + RekeyLimit []string // . + RevokedKeys string // * + RDomain string // * + SetEnv sshEnv // * // max is 4095, it goes in the config as an octal. StreamLocalBindMask uint16 // * StreamLocalBindUnlink sshBool // * @@ -101,7 +101,7 @@ type SshdConf struct { HostCertificate string // * HostKeyAgent string // * HostKeyAlgorithms []string // +* - HostKey []string // . + HostKey []string // .+ IgnoreUserKnownHosts sshBool // . KerberosGetAFSToken sshBool // . KerberosOrLocalPasswd sshBool // . @@ -114,7 +114,7 @@ type SshdConf struct { MaxStartups string // . PermitUserEnvironment sshBool // . PidFile string // . - Port uint16 // . + Port []uint16 // . PrintLastLog sshBool // .+ PrintMotd sshBool // . Protocol int // +* @@ -131,20 +131,86 @@ type SshdConf struct { XAuthLocation string // * } -// SshConf represents an /etc/ssh/ssh_config (or ~/.ssh/config) file -type SshConf struct { - // These are in the default upstream sshd_config so we don't touch them. (Most, if not all, are commented out.) - // We just have them here to parse them. - Host map[string]string -} - +// ListenAddr is a parsed ListenAddress directive. type ListenAddr struct { Addr string // hostname|address, hostname:port, IPv4_address:port, or [hostname|address]:port in conf string. Port uint16 RDomain string } +// MatchSshd is an sshd_config Match block. type MatchSshd struct { Criteria map[string]string Rules []SshdMatchRule } + +// SshMatchRule is more or less a subset of SshConf. These are valid keywords for Match blocks in sshd_config. +type SshMatchRule struct { +} + +// SshConf represents an /etc/ssh/ssh_config (or ~/.ssh/config) file +type SshConf struct { + AddKeysToAgent string // * + AddressFamily string // . + BatchMode sshBool // . + BindAddress string // * + BindInterface string // * + CanonicalDomains []string // * + CanonicalizeFallbackLocal sshBool // * + CanonicalizeHostname sshBool // * + CanonicalizeMaxDots uint8 // * + CanonicalizePermittedCNAMEs [][2]string // * + CASignatureAlgorithms []string // * + CertificateFile string // * + ChallengeResponseAuthentication sshBool // * + CheckHostIP sshBool // .+ + Ciphers []string // .+ + ClearAllForwardings sshBool // * + Compression sshBool // * + ConnectionAttempts uint16 // * + ConnectTimeout uint16 // . + ControlMaster string // * + ControlPath string // * + ControlPersist string // * + EnableSSHKeysign sshBool // * + EscapeChar string // . + ExitOnForwardFailure sshBool // * + FingerprintHash string // * + ForwardAgent string // . + ForwardXll sshBool // . + ForwardX11Timeout string // . + ForwardX11Trusted sshBool // * + GatewayPorts sshBool // * + GlobalKnownHostsFile []string // * + GSSAPIAuthentication sshBool // . + GSSAPIDelegateCredentials sshBool // . + HashKnownHosts sshBool // *+ + Host []HostSsh // . + HostbasedAuthentication sshBool // . + HostbasedKeyTypes []string // *+ + HostKeyAlgorithms []string // *+ + HostKeyAlias string // * + Hostname string // * + IdentitiesOnly sshBool // * + IdentityAgent string // * + IdentityFile []string // . + IgnoreUnknown []string // * + Include string // * + IPQoS string // * + KbdInteractiveAuthentication sshBool // * + KbdInteractiveDevices []string // * + KexAlgorithms []string // * + LocalCommand string // * + LocalForward [2]string // * +} + +// MatchSsh is an ssh_config Match block. +type MatchSsh struct { + Criteria map[string]string + Rules []SshMatchRule +} + +type HostSsh struct { + Hostname string + Rules []SshMatchRule +} diff --git a/config/type.go b/config/type.go index a54dc27..7bcea69 100644 --- a/config/type.go +++ b/config/type.go @@ -1,5 +1,9 @@ package config +import ( + `fmt` +) + type sshBool bool func (b sshBool) Str() string { @@ -8,3 +12,13 @@ func (b sshBool) Str() string { } return "no" } + +type sshEnv map[string]string + +func (e sshEnv) Str() string { + var s string + for k, v := range e { + s += fmt.Sprintf("%v=%v", k, v) + } + return s +} \ No newline at end of file diff --git a/config/validator.go b/config/validator.go index 16052b3..5afe171 100644 --- a/config/validator.go +++ b/config/validator.go @@ -1,34 +1,167 @@ package config import ( - "fmt" - - "github.com/oleiade/reflections" - "github.com/pkg/errors" + `errors` + `fmt` + `reflect` + `strings` ) var err error -func (c *SshdConf) Validate() (bool, error) { - for k, v := range validSshdSingleVals { - realV, err := reflections.GetField(c, k) - if err != nil { - return false, err - } - valid := false - for _, i := range v { - if i == realV { - valid = true - break +// TODO: Match block validation + +// Have I mentioned how dumb golang is yet? The following methods share 90% of the same code, +// but ironically delegating that to a separate function is going to be even more of a PITA for now +// than just maintaining two separate methods. +// How dumb. + +// Validate validates the actual struct of an SshdConf itself, ensuring that certain list fields contain +// valid item(s). It collects all found issues in allErr. +func (c *SshdConf) Validate() (validConf bool, allErr []error) { + // Set the default as true. + // We set it on false on first failed validation and keep going. + validConf = true + fields := reflect.TypeOf(c) + values := reflect.ValueOf(c) + for i := 0; i < fields.NumField(); i++ { + field := fields.Field(i) + value := values.Field(i) + var validField bool + n := field.Name + single, isSingle := validSshdSingleVals[n] + multi, isMulti := validSshdMultiVals[n] + if isSingle { + v := value.String() + for _, x := range single { + if v == x { + validField = true + break + } + } + if !validField { + validConf = false + e := fmt.Sprintf( + "field %v: value %v is not allowed; must be ONE of: %v", + n, v, single, + ) + allErr = append(allErr, errors.New(e)) + } + } else if isMulti { + // These are all []strings if the field name is in validSshdMultiVals. + // But golang is still dumb and has no way of serializing directly, so here comes the hackery. + v, ok := value.Interface().([]string) + if !ok { + validConf = false + e := fmt.Sprintf( + "field %v: value %v was not able to be parsed as a []string", + n, v, + ) + allErr = append(allErr, errors.New(e)) + } + var invalidSlice []string + for _, vStr := range v { + validItem := false + for _, strip := range sshdStripPre { + if n == strip { + for _, c := range []string{"+", "-", "^"} { + vStr = strings.TrimPrefix(vStr, c) + } + + } + } + for _, x := range multi { + if vStr == x { + validItem = true + } + } + if !validItem { + invalidSlice = append(invalidSlice, vStr) + } + } + for _, x := range invalidSlice { + e := fmt.Sprintf( + "field %v: value item %v is not valid; it must be one or more of %v", + n, x, multi, + ) + allErr = append(allErr, errors.New(e)) } } - if !valid { - e := fmt.Sprintf( - "field %v value %v is not allowed", - k, realV) - return false, errors.New(e) + } + return validConf, allErr +} + +// Validate validates the actual struct of an SshConf itself, ensuring that certain list fields contain +// valid item(s). It collects all found issues in allErr. +func (c *SshConf) Validate() (validConf bool, allErr []error) { + // Set the default as true. + // We set it on false on first failed validation and keep going. + validConf = true + fields := reflect.TypeOf(c) + values := reflect.ValueOf(c) + for i := 0; i < fields.NumField(); i++ { + field := fields.Field(i) + value := values.Field(i) + var validField bool + n := field.Name + single, isSingle := validSshSingleVals[n] + multi, isMulti := validSshMultiVals[n] + if isSingle { + v := value.String() + for _, x := range single { + if v == x { + validField = true + break + } + } + if !validField { + validConf = false + e := fmt.Sprintf( + "field %v: value %v is not allowed; must be ONE of: %v", + n, v, single, + ) + allErr = append(allErr, errors.New(e)) + } + } else if isMulti { + // These are all []strings if the field name is in validSshMultiVals. + // But golang is still dumb and has no way of serializing directly, so here comes the hackery. + v, ok := value.Interface().([]string) + if !ok { + validConf = false + e := fmt.Sprintf( + "field %v: value %v was not able to be parsed as a []string", + n, v, + ) + allErr = append(allErr, errors.New(e)) + } + var invalidSlice []string + for _, vStr := range v { + validItem := false + for _, strip := range sshStripPre { + if n == strip { + for _, c := range []string{"+", "-", "^"} { + vStr = strings.TrimPrefix(vStr, c) + } + + } + } + for _, x := range multi { + if vStr == x { + validItem = true + } + } + if !validItem { + invalidSlice = append(invalidSlice, vStr) + } + } + for _, x := range invalidSlice { + e := fmt.Sprintf( + "field %v: value item %v is not valid; it must be one or more of %v", + n, x, multi, + ) + allErr = append(allErr, errors.New(e)) + } } } - - return true, nil + return validConf, allErr }