checking in some config stuff

This commit is contained in:
brent s. 2020-10-07 11:25:56 -04:00
parent 4b912a8dae
commit d3a5f039c1
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
5 changed files with 349 additions and 67 deletions

1
config/TODO Normal file
View File

@ -0,0 +1 @@
add default config as struct

View File

@ -38,17 +38,20 @@ const (
var ( var (
// sshdModify are values we modify. // sshdModify are values we modify.
sshdModify = [...]string{ sshdModify = [...]string{
"AuthenticationMethods",
"ChallengeResponseAuthentication",
"Ciphers",
"HostKey", "HostKey",
"PermitRootLogin", "HostKeyAlgorithms",
"StrictModes", "KexAlgorithms",
"PubkeyAuthentication", "MACs",
"PasswordAuthentication", "PasswordAuthentication",
"PermitEmptyPasswords", "PermitEmptyPasswords",
"ChallengeResponseAuthentication", "PermitRootLogin",
"KexAlgorithms", "PrintLastLog",
"Protocol", "Protocol",
"Ciphers", "PubkeyAuthentication",
"MACs", "StrictModes",
} }


// sshModify are values we modify. // sshModify are values we modify.
@ -62,6 +65,10 @@ var (
"", "",
} }


sshMulti = [...]string{
"IdentityFile",
}

// authMethods are authentication methods that openssh supports. // authMethods are authentication methods that openssh supports.
authMethods = []string{ authMethods = []string{
"any", "any",
@ -92,11 +99,13 @@ var (
// forwardAllows are shared values used by forwarding access control. // forwardAllows are shared values used by forwarding access control.
forwardAllows = []string{"yes", "all", "no", "local", "remote"} 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. // The following should generate the same list.
// ssh -Q HostKeyAlgorithms | sed -re 's/^/"/g' -e 's/$/",/g' // ssh -Q HostKeyAlgorithms | sed -re 's/^/"/g' -e 's/$/",/g'
// ssh -Q HostbasedAcceptedKeyTypes | 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",
"ssh-ed25519-cert-v01@openssh.com", "ssh-ed25519-cert-v01@openssh.com",
"sk-ssh-ed25519@openssh.com", "sk-ssh-ed25519@openssh.com",
@ -196,6 +205,21 @@ var (
"DEBUG3", "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. // 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' // ssh -Q mac | sed -re 's/^/"/g' -e 's/$/",/g'
macAlgos = []string{ macAlgos = []string{
@ -224,6 +248,7 @@ var (
// Valid keys for sshdMatchCriteria are tracked via field names in SshdMatchRule. // 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" // Multiple criteria can be specified by e.g. "Match User foo, Host bar.tld"
sshdMatchCriteria = []string{ sshdMatchCriteria = []string{
"Address",
"All", "All",
"User", "User",
"Group", "Group",
@ -231,18 +256,37 @@ var (
"LocalAddress", "LocalAddress",
"LocalPort", "LocalPort",
"RDomain", "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. // The following are validator maps.
var ( var (
// These directives can also begin with "+", "-", or "^", so they need to be stripped off. // 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{ sshdStripPre = []string{
"HostbasedAcceptedKeyTypes", "HostbasedAcceptedKeyTypes",
"KexAlgorithms", "KexAlgorithms",
} }
sshStripPre = []string{
"HostbasedKeyTypes",
"KexAlgorithms",
}

// validSshdSingleVals are values that can accept a single string value from a static list. // validSshdSingleVals are values that can accept a single string value from a static list.
validSshdSingleVals = map[string][]string{ validSshdSingleVals = map[string][]string{
"AddressFamily": {"any", "inet", "inet6"}, "AddressFamily": {"any", "inet", "inet6"},
@ -251,8 +295,13 @@ var (
"Compression": {"yes", "delayed", "no"}, // "delayed" is legacy, same as "yes". "Compression": {"yes", "delayed", "no"}, // "delayed" is legacy, same as "yes".
"FingerprintHash": {"sha256", "md5"}, "FingerprintHash": {"sha256", "md5"},
"GatewayPorts": {"yes", "no", "clientspecified"}, "GatewayPorts": {"yes", "no", "clientspecified"},
"HostbasedAcceptedKeyTypes": keyTypes,
"IgnoreRHosts": {"yes", "shosts-only", "no"}, "IgnoreRHosts": {"yes", "shosts-only", "no"},
"LogLevel": logLevels, "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. // validSshdMultiVals are values that can accept multiple values from a static list.
@ -260,9 +309,28 @@ var (
"AuthenticationMethods": authMethods, "AuthenticationMethods": authMethods,
"CASignatureAlgorithms": sigAlgos, "CASignatureAlgorithms": sigAlgos,
"Ciphers": ciphers, "Ciphers": ciphers,
"HostbasedAcceptedKeyTypes": hostkeyTypes, // NOTE: Can also begin with "+", "-", or "^" "HostbasedAcceptedKeyTypes": keyTypes,
"HostKeyAlgorithms": hostkeyTypes, "HostKeyAlgorithms": keyTypes,
"KexAlgorithms": kexAlgos, // NOTE: Can also begin with "+", "-", or "^" "KexAlgorithms": kexAlgos,
"MACs": macAlgos, "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,
} }
) )

View File

@ -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)). *: 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 { type SshdMatchRule struct {
AcceptEnv []string // * AcceptEnv []string // *
AllowAgentForwarding sshBool // . AllowAgentForwarding sshBool // .
@ -49,33 +49,33 @@ type SshdMatchRule struct {
ForceCommand string // * ForceCommand string // *
GatewayPorts string // . GatewayPorts string // .
GSSAPIAuthentication sshBool // . GSSAPIAuthentication sshBool // .
HostbasedAcceptedKeyTypes []string // * HostbasedAcceptedKeyTypes []string // *+
HostbasedAuthentication sshBool // . HostbasedAuthentication sshBool // .
HostbasedUsesNameFromPacketOnly sshBool // * HostbasedUsesNameFromPacketOnly sshBool // *
IgnoreRhosts string // . IgnoreRhosts string // .
// Do we handle includes? Or just let sshd -T handle it? // Do we handle includes? Or just let sshd -T handle it?
Include string // * Include string // *
// Accepts one or two. If two, first is interactive and second is non-interactive. // Accepts one or two. If two, first is interactive and second is non-interactive.
IPQoS [2]string // * IPQoS [2]string // *
KbdInteractiveAuthentication sshBool // * KbdInteractiveAuthentication sshBool // *
KerberosAuthentication sshBool // . KerberosAuthentication sshBool // .
LogLevel string // . LogLevel string // .
MaxAuthTries int // . MaxAuthTries int // .
MaxSessions int // . MaxSessions int // .
PasswordAuthentication sshBool // .+ PasswordAuthentication sshBool // .+
PermitEmptyPasswords sshBool // + PermitEmptyPasswords sshBool // +
PermitListen string // * PermitListen string // *
PermitOpen string // * PermitOpen string // *
PermitRootLogin string // .+ PermitRootLogin string // .+
PermitTTY sshBool // . PermitTTY sshBool // .
PermitTunnel string // . PermitTunnel string // .
PermitUserRC sshBool // * PermitUserRC sshBool // *
PubkeyAcceptedKeyTypes []string // * PubkeyAcceptedKeyTypes []string // *
PubkeyAuthentication sshBool // .+ PubkeyAuthentication sshBool // .+
RekeyLimit string // . RekeyLimit []string // .
RevokedKeys string // * RevokedKeys string // *
RDomain string // * RDomain string // *
SetEnv map[string]string // * SetEnv sshEnv // *
// max is 4095, it goes in the config as an octal. // max is 4095, it goes in the config as an octal.
StreamLocalBindMask uint16 // * StreamLocalBindMask uint16 // *
StreamLocalBindUnlink sshBool // * StreamLocalBindUnlink sshBool // *
@ -101,7 +101,7 @@ type SshdConf struct {
HostCertificate string // * HostCertificate string // *
HostKeyAgent string // * HostKeyAgent string // *
HostKeyAlgorithms []string // +* HostKeyAlgorithms []string // +*
HostKey []string // . HostKey []string // .+
IgnoreUserKnownHosts sshBool // . IgnoreUserKnownHosts sshBool // .
KerberosGetAFSToken sshBool // . KerberosGetAFSToken sshBool // .
KerberosOrLocalPasswd sshBool // . KerberosOrLocalPasswd sshBool // .
@ -114,7 +114,7 @@ type SshdConf struct {
MaxStartups string // . MaxStartups string // .
PermitUserEnvironment sshBool // . PermitUserEnvironment sshBool // .
PidFile string // . PidFile string // .
Port uint16 // . Port []uint16 // .
PrintLastLog sshBool // .+ PrintLastLog sshBool // .+
PrintMotd sshBool // . PrintMotd sshBool // .
Protocol int // +* Protocol int // +*
@ -131,20 +131,86 @@ type SshdConf struct {
XAuthLocation string // * XAuthLocation string // *
} }


// SshConf represents an /etc/ssh/ssh_config (or ~/.ssh/config) file // ListenAddr is a parsed ListenAddress directive.
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
}

type ListenAddr struct { type ListenAddr struct {
Addr string // hostname|address, hostname:port, IPv4_address:port, or [hostname|address]:port in conf string. Addr string // hostname|address, hostname:port, IPv4_address:port, or [hostname|address]:port in conf string.
Port uint16 Port uint16
RDomain string RDomain string
} }


// MatchSshd is an sshd_config Match block.
type MatchSshd struct { type MatchSshd struct {
Criteria map[string]string Criteria map[string]string
Rules []SshdMatchRule 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
}

View File

@ -1,5 +1,9 @@
package config package config


import (
`fmt`
)

type sshBool bool type sshBool bool


func (b sshBool) Str() string { func (b sshBool) Str() string {
@ -8,3 +12,13 @@ func (b sshBool) Str() string {
} }
return "no" 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
}

View File

@ -1,34 +1,167 @@
package config package config


import ( import (
"fmt" `errors`

`fmt`
"github.com/oleiade/reflections" `reflect`
"github.com/pkg/errors" `strings`
) )


var err error var err error


func (c *SshdConf) Validate() (bool, error) { // TODO: Match block validation
for k, v := range validSshdSingleVals {
realV, err := reflections.GetField(c, k) // Have I mentioned how dumb golang is yet? The following methods share 90% of the same code,
if err != nil { // but ironically delegating that to a separate function is going to be even more of a PITA for now
return false, err // than just maintaining two separate methods.
} // How dumb.
valid := false
for _, i := range v { // Validate validates the actual struct of an SshdConf itself, ensuring that certain list fields contain
if i == realV { // valid item(s). It collects all found issues in allErr.
valid = true func (c *SshdConf) Validate() (validConf bool, allErr []error) {
break // 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( return validConf, allErr
"field %v value %v is not allowed", }
k, realV)
return false, errors.New(e) // 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 validConf, allErr
return true, nil
} }