all the directives are copied in with their types. working on validators now.
This commit is contained in:
parent
c22786204a
commit
4b912a8dae
@ -55,6 +55,7 @@ It will create backups of any file(s) it replaces and automatically rolls back `
|
|||||||
configuration changes if it does not pass the syntax check (`sshd -t`) to avoid
|
configuration changes if it does not pass the syntax check (`sshd -t`) to avoid
|
||||||
accidentally locking you out.
|
accidentally locking you out.
|
||||||
|
|
||||||
|
<!--
|
||||||
The first time you run it, it will quite possibly take a **very** long time. This is
|
The first time you run it, it will quite possibly take a **very** long time. This is
|
||||||
because it's generating fresh DH parameters, which is a very time-consuming process.
|
because it's generating fresh DH parameters, which is a very time-consuming process.
|
||||||
Subsequent runs will not take as long, however, as checks are put in place to determine
|
Subsequent runs will not take as long, however, as checks are put in place to determine
|
||||||
@ -62,6 +63,7 @@ if custom DH parameters have been generated or not yet. If it's running on a GNU
|
|||||||
system and you have [`haveged`](http://www.issihosts.com/haveged/) installed, that will
|
system and you have [`haveged`](http://www.issihosts.com/haveged/) installed, that will
|
||||||
significantly speed up the process (SSHSecure will start it automatically if it isn't
|
significantly speed up the process (SSHSecure will start it automatically if it isn't
|
||||||
running already).
|
running already).
|
||||||
|
-->
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
|
27
TODO
27
TODO
@ -1,27 +1,24 @@
|
|||||||
-sshkeys (see ref/<type>/parse_poc_<keytype>.go for POC)
|
|
||||||
--hostkeys (https://security.stackexchange.com/questions/211106/what-is-the-difference-between-host-and-client-ssh-key-generation)?
|
|
||||||
-moduli dhparams generation (dh.c? moduli.c?)
|
|
||||||
--ssh-keygen.c, ~L3565
|
|
||||||
|
|
||||||
- General/common
|
- General/common
|
||||||
-- Locking?
|
-- HALF-DONE: Locking?
|
||||||
-- Constants for common file dests
|
-- DONE: Constants for common file dests
|
||||||
-- Func to write to dest, backing up dest if exists first
|
-- DONE: Func to write to dest, backing up dest if exists first
|
||||||
-- Test ssh config (sshd -t) and rollback if fail
|
-- DONE: Test ssh config (sshd -t) and rollback if fail
|
||||||
|
-- When completely done, go.mod
|
||||||
|
|
||||||
- Key generation
|
- Key generation
|
||||||
-- DONE: Generate priv/pubkeys
|
-- DONE: Generate priv/pubkeys
|
||||||
-- Build key structure
|
-- DONE: Build key structure
|
||||||
-- write out base64 with headers to files
|
-- write out base64 with headers to files
|
||||||
|
|
||||||
- SSH Moduli
|
- SSH Moduli
|
||||||
-- Do DH param gen in goroutine so we can do other things while it spawns and runs
|
-- NOT NEEDED?: Do DH param gen in goroutine so we can do other things while it spawns and runs
|
||||||
-- Check if haveged is running. If not and installed, start it.
|
--- Check if haveged is running. If not and installed, start it.
|
||||||
-- Generate moduli
|
--- Generate moduli
|
||||||
-- Render to /etc/ssh/moduli format
|
-- DONE: Render to /etc/ssh/moduli format
|
||||||
--- custom moduli marshaler/unmarshaler? (e.g. https://stackoverflow.com/a/50211222)
|
--- DONE: custom moduli marshaler/unmarshaler? (e.g. https://stackoverflow.com/a/50211222)
|
||||||
-- Write to dest
|
-- Write to dest
|
||||||
|
|
||||||
- Config
|
- Config
|
||||||
-- Need to merge in changes
|
-- Need to merge in changes
|
||||||
-- Track options in struct?
|
-- Track options in struct?
|
||||||
|
--- add subtype structs
|
||||||
|
268
config/const.go
Normal file
268
config/const.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
// To check if this release of SSHSecure is up-to-date with the default values.
|
||||||
|
// upstreamSshdURL string = "https://anongit.mindrot.org/openssh.git/plain/sshd_config"
|
||||||
|
upstreamSshdURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/sshd_config"
|
||||||
|
upstreamSshdCksum string = "952C844D7B36C54B03E2ADFB24860405" +
|
||||||
|
"1A702620A0ADC0738A8C30DC83D42A75" +
|
||||||
|
"27F5B3C184E779B1430168950F7695A1" +
|
||||||
|
"AA249F7CC719DEC1631ACFDDC2E8B653"
|
||||||
|
// upstreamSshURL string = "https://anongit.mindrot.org/openssh.git/plain/ssh_config"
|
||||||
|
upstreamSshURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/ssh_config"
|
||||||
|
upstreamSshCksum string = "FF2D600465CC5D9CFBB57346491CCAF2" +
|
||||||
|
"C917E2F0C7B4D4EF6B851940948B55BD" +
|
||||||
|
"88205AC8153210ECA6C6BEA38E800F33" +
|
||||||
|
"7562C0190AE760220417A5DC2E00A5E1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are items that this program modifies.
|
||||||
|
var (
|
||||||
|
// sshdModify are values we modify.
|
||||||
|
sshdModify = [...]string{
|
||||||
|
"HostKey",
|
||||||
|
"PermitRootLogin",
|
||||||
|
"StrictModes",
|
||||||
|
"PubkeyAuthentication",
|
||||||
|
"PasswordAuthentication",
|
||||||
|
"PermitEmptyPasswords",
|
||||||
|
"ChallengeResponseAuthentication",
|
||||||
|
"KexAlgorithms",
|
||||||
|
"Protocol",
|
||||||
|
"Ciphers",
|
||||||
|
"MACs",
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshModify are values we modify.
|
||||||
|
sshModify = [...]string{""}
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are collections of long lists of valid values.
|
||||||
|
var (
|
||||||
|
// sshdMulti are values that can be specified multiple times (multiple lines).
|
||||||
|
sshdMulti = [...]string{
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
// authMethods are authentication methods that openssh supports.
|
||||||
|
authMethods = []string{
|
||||||
|
"any",
|
||||||
|
"keyboard-interactive",
|
||||||
|
"keyboard-interactive:bsdauth",
|
||||||
|
"keyboard-interactive:pam",
|
||||||
|
"gssapi-with-mic",
|
||||||
|
"hostbased",
|
||||||
|
"none",
|
||||||
|
"password",
|
||||||
|
"publickey",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ciphers are cipher algorithms that openssh supports.
|
||||||
|
ciphers = []string{
|
||||||
|
"3des-cbc",
|
||||||
|
"aes128-cbc",
|
||||||
|
"aes192-cbc",
|
||||||
|
"aes256-cbc",
|
||||||
|
"aes128-ctr",
|
||||||
|
"aes192-ctr",
|
||||||
|
"aes256-ctr",
|
||||||
|
"aes128-gcm@openssh.com",
|
||||||
|
"aes256-gcm@openssh.com",
|
||||||
|
"chacha20-poly1305@openssh.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardAllows are shared values used by forwarding access control.
|
||||||
|
forwardAllows = []string{"yes", "all", "no", "local", "remote"}
|
||||||
|
|
||||||
|
// hostkeyTypes are algorithms/types used for host keys.
|
||||||
|
// 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-ed25519",
|
||||||
|
"ssh-ed25519-cert-v01@openssh.com",
|
||||||
|
"sk-ssh-ed25519@openssh.com",
|
||||||
|
"sk-ssh-ed25519-cert-v01@openssh.com",
|
||||||
|
"ssh-rsa",
|
||||||
|
"rsa-sha2-256",
|
||||||
|
"rsa-sha2-512",
|
||||||
|
"ssh-dss",
|
||||||
|
"ecdsa-sha2-nistp256",
|
||||||
|
"ecdsa-sha2-nistp384",
|
||||||
|
"ecdsa-sha2-nistp521",
|
||||||
|
"sk-ecdsa-sha2-nistp256@openssh.com",
|
||||||
|
"ssh-rsa-cert-v01@openssh.com",
|
||||||
|
"rsa-sha2-256-cert-v01@openssh.com",
|
||||||
|
"rsa-sha2-512-cert-v01@openssh.com",
|
||||||
|
"ssh-dss-cert-v01@openssh.com",
|
||||||
|
"ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||||
|
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
|
||||||
|
"ecdsa-sha2-nistp521-cert-v01@openssh.com",
|
||||||
|
"sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ipQoS is a list of valid QoS profiles.
|
||||||
|
ipQoS = []string{
|
||||||
|
// This also supports a "numeric value" per sshd_config(5),
|
||||||
|
// but I have no idea what those values are, their range, etc.
|
||||||
|
// So strings only. Makes for more readable configs anyways.
|
||||||
|
// TODO: is this specified in the source anywhere?
|
||||||
|
"af11",
|
||||||
|
"af12",
|
||||||
|
"af13",
|
||||||
|
"af21",
|
||||||
|
"af22",
|
||||||
|
"af23",
|
||||||
|
"af31",
|
||||||
|
"af32",
|
||||||
|
"af33",
|
||||||
|
"af41",
|
||||||
|
"af42",
|
||||||
|
"af43",
|
||||||
|
"cs0",
|
||||||
|
"cs1",
|
||||||
|
"cs2",
|
||||||
|
"cs3",
|
||||||
|
"cs4",
|
||||||
|
"cs5",
|
||||||
|
"cs6",
|
||||||
|
"cs7",
|
||||||
|
"ef",
|
||||||
|
"le",
|
||||||
|
"lowdelay",
|
||||||
|
"throughput",
|
||||||
|
"reliability",
|
||||||
|
"none",
|
||||||
|
}
|
||||||
|
|
||||||
|
// kexAlgos is a lost of valid kex ("KEy eXchange") algorithms.
|
||||||
|
// ssh -Q kex | sed -re 's/^/"/g' -e 's/$/",/g'
|
||||||
|
kexAlgos = []string{
|
||||||
|
"diffie-hellman-group1-sha1",
|
||||||
|
"diffie-hellman-group14-sha1",
|
||||||
|
"diffie-hellman-group14-sha256",
|
||||||
|
"diffie-hellman-group16-sha512",
|
||||||
|
"diffie-hellman-group18-sha512",
|
||||||
|
"diffie-hellman-group-exchange-sha1",
|
||||||
|
"diffie-hellman-group-exchange-sha256",
|
||||||
|
"ecdh-sha2-nistp256",
|
||||||
|
"ecdh-sha2-nistp384",
|
||||||
|
"ecdh-sha2-nistp521",
|
||||||
|
"curve25519-sha256",
|
||||||
|
"curve25519-sha256@libssh.org",
|
||||||
|
"sntrup4591761x25519-sha512@tinyssh.org",
|
||||||
|
}
|
||||||
|
|
||||||
|
// sigAlgos is a list of valid algorithms for CA signatures.
|
||||||
|
sigAlgos = []string{
|
||||||
|
// These are the defaults. TODO: are any others valid?
|
||||||
|
"ecdsa-sha2-nistp256",
|
||||||
|
"ecdsa-sha2-nistp384",
|
||||||
|
"ecdsa-sha2-nistp521",
|
||||||
|
"ssh-ed25519",
|
||||||
|
"rsa-sha2-512",
|
||||||
|
"rsa-sha2-256",
|
||||||
|
"ssh-rsa",
|
||||||
|
}
|
||||||
|
|
||||||
|
// logLevels is a list of valid LogLevel levels.
|
||||||
|
logLevels = []string{
|
||||||
|
"QUIET",
|
||||||
|
"FATAL",
|
||||||
|
"ERROR",
|
||||||
|
"INFO", // default
|
||||||
|
"VERBOSE",
|
||||||
|
"DEBUG", // same as DEBUG1
|
||||||
|
"DEBUG1", // same as DEBUG
|
||||||
|
"DEBUG2",
|
||||||
|
"DEBUG3",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
"hmac-sha1",
|
||||||
|
"hmac-sha1-96",
|
||||||
|
"hmac-sha2-256",
|
||||||
|
"hmac-sha2-512",
|
||||||
|
"hmac-md5",
|
||||||
|
"hmac-md5-96",
|
||||||
|
"umac-64@openssh.com",
|
||||||
|
"umac-128@openssh.com",
|
||||||
|
"hmac-sha1-etm@openssh.com",
|
||||||
|
"hmac-sha1-96-etm@openssh.com",
|
||||||
|
"hmac-sha2-256-etm@openssh.com",
|
||||||
|
"hmac-sha2-512-etm@openssh.com",
|
||||||
|
"hmac-md5-etm@openssh.com",
|
||||||
|
"hmac-md5-96-etm@openssh.com",
|
||||||
|
"umac-64-etm@openssh.com",
|
||||||
|
"umac-128-etm@openssh.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a collection related to Match blocks.
|
||||||
|
var (
|
||||||
|
// sshdMatchCriteria is a list of valid criteria that can be used in a Match block.
|
||||||
|
// 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{
|
||||||
|
"All",
|
||||||
|
"User",
|
||||||
|
"Group",
|
||||||
|
"Host",
|
||||||
|
"LocalAddress",
|
||||||
|
"LocalPort",
|
||||||
|
"RDomain",
|
||||||
|
"Address",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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?
|
||||||
|
sshdStripPre = []string{
|
||||||
|
"HostbasedAcceptedKeyTypes",
|
||||||
|
"KexAlgorithms",
|
||||||
|
}
|
||||||
|
// validSshdSingleVals are values that can accept a single string value from a static list.
|
||||||
|
validSshdSingleVals = map[string][]string{
|
||||||
|
"AddressFamily": {"any", "inet", "inet6"},
|
||||||
|
"AllowStreamLocalForwarding": forwardAllows,
|
||||||
|
"AllowTcpForwarding": forwardAllows,
|
||||||
|
"Compression": {"yes", "delayed", "no"}, // "delayed" is legacy, same as "yes".
|
||||||
|
"FingerprintHash": {"sha256", "md5"},
|
||||||
|
"GatewayPorts": {"yes", "no", "clientspecified"},
|
||||||
|
"IgnoreRHosts": {"yes", "shosts-only", "no"},
|
||||||
|
"LogLevel": logLevels,
|
||||||
|
}
|
||||||
|
|
||||||
|
// validSshdMultiVals are values that can accept multiple values from a static list.
|
||||||
|
validSshdMultiVals = map[string][]string{
|
||||||
|
"AuthenticationMethods": authMethods,
|
||||||
|
"CASignatureAlgorithms": sigAlgos,
|
||||||
|
"Ciphers": ciphers,
|
||||||
|
"HostbasedAcceptedKeyTypes": hostkeyTypes, // NOTE: Can also begin with "+", "-", or "^"
|
||||||
|
"HostKeyAlgorithms": hostkeyTypes,
|
||||||
|
"KexAlgorithms": kexAlgos, // NOTE: Can also begin with "+", "-", or "^"
|
||||||
|
"MACs": macAlgos,
|
||||||
|
}
|
||||||
|
)
|
81
config/func.go
Normal file
81
config/func.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"r00t2.io/sysutils/exec_extra"
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig(config *[]byte) (bool, error) {
|
||||||
|
var sysPaths []string
|
||||||
|
var binPath string
|
||||||
|
var tmpConf *os.File
|
||||||
|
var err error
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
// sshd *requires* to be invoked with an absolute path.
|
||||||
|
sysPaths, err = paths.GetPathEnv()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, p := range sysPaths {
|
||||||
|
fpath := path.Join(p, "sshd")
|
||||||
|
if exists, err := paths.RealPathExists(&fpath); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
binPath = fpath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tmpConf, err = ioutil.TempFile("/tmp", ".test.sshconf.")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpConf.Name())
|
||||||
|
if err = tmpConf.Chmod(0600); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if _, err = tmpConf.Write(*config); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(binPath,
|
||||||
|
"-T", fmt.Sprintf("-f %v",
|
||||||
|
tmpConf.Name()))
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
exitStatus, err := exec_extra.ExecCmdReturn(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if exitStatus != 0 {
|
||||||
|
// TODO: also handle non-empty stderr?
|
||||||
|
e := fmt.Sprintf("returned status/exit code %d", exitStatus)
|
||||||
|
return false, errors.New(e)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
150
config/struct.go
Normal file
150
config/struct.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTATION KEY:
|
||||||
|
.: Exists in default upstream config (but usually they're commented out)
|
||||||
|
+: These values are/may be modified by this program.
|
||||||
|
*: 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.
|
||||||
|
type SshdMatchRule struct {
|
||||||
|
AcceptEnv []string // *
|
||||||
|
AllowAgentForwarding sshBool // .
|
||||||
|
AllowGroups []string // *
|
||||||
|
AllowStreamLocalForwarding string // *
|
||||||
|
AllowTcpForwarding string // .
|
||||||
|
AllowUsers []string // *
|
||||||
|
AuthenticationMethods []string // +*
|
||||||
|
AuthorizedKeysCommand string // .
|
||||||
|
AuthorizedKeysCommandUser string // .
|
||||||
|
AuthorizedKeysFile string // .
|
||||||
|
AuthorizedPrincipalsCommand string // *
|
||||||
|
AuthorizedPrincipalsCommandUser string // *
|
||||||
|
AuthorizedPrincipalsFile string // .
|
||||||
|
Banner string // .
|
||||||
|
ChrootDirectory string // .
|
||||||
|
ClientAliveCountMax int // .
|
||||||
|
ClientAliveInterval int // .
|
||||||
|
DenyGroups []string // *
|
||||||
|
DenyUsers []string // *
|
||||||
|
ForceCommand string // *
|
||||||
|
GatewayPorts string // .
|
||||||
|
GSSAPIAuthentication sshBool // .
|
||||||
|
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 // *
|
||||||
|
// max is 4095, it goes in the config as an octal.
|
||||||
|
StreamLocalBindMask uint16 // *
|
||||||
|
StreamLocalBindUnlink sshBool // *
|
||||||
|
TrustedUserCAKeys string // *
|
||||||
|
X11DisplayOffset int // .
|
||||||
|
X11Forwarding sshBool // .
|
||||||
|
}
|
||||||
|
|
||||||
|
// SshdConf represents an /etc/ssh/sshd_config file's directives/values.
|
||||||
|
// Values in SshdMatchRule are not reproduced here.
|
||||||
|
type SshdConf struct {
|
||||||
|
SshdMatchRule
|
||||||
|
AddressFamily string // .
|
||||||
|
CASignatureAlgorithms []string // *
|
||||||
|
ChallengeResponseAuthentication sshBool // .+
|
||||||
|
Ciphers []string // +*
|
||||||
|
Compression string // .
|
||||||
|
DisableForwarding sshBool // *
|
||||||
|
ExposeAuthInfo sshBool // *
|
||||||
|
FingerprintHash string // *
|
||||||
|
GSSAPICleanupCredentials sshBool // .
|
||||||
|
GSSAPIStrictAcceptorCheck sshBool // *
|
||||||
|
HostCertificate string // *
|
||||||
|
HostKeyAgent string // *
|
||||||
|
HostKeyAlgorithms []string // +*
|
||||||
|
HostKey []string // .
|
||||||
|
IgnoreUserKnownHosts sshBool // .
|
||||||
|
KerberosGetAFSToken sshBool // .
|
||||||
|
KerberosOrLocalPasswd sshBool // .
|
||||||
|
KerberosTicketCleanup sshBool // .
|
||||||
|
KexAlgorithms string // +*
|
||||||
|
ListenAddress ListenAddr // .
|
||||||
|
LoginGraceTime string // .
|
||||||
|
MACs []string // +*
|
||||||
|
Match map[string]string // .
|
||||||
|
MaxStartups string // .
|
||||||
|
PermitUserEnvironment sshBool // .
|
||||||
|
PidFile string // .
|
||||||
|
Port uint16 // .
|
||||||
|
PrintLastLog sshBool // .+
|
||||||
|
PrintMotd sshBool // .
|
||||||
|
Protocol int // +*
|
||||||
|
PubkeyAuthOptions string // *
|
||||||
|
SecurityKeyProvider string // *
|
||||||
|
StrictModes sshBool // .+
|
||||||
|
Subsystem string // .
|
||||||
|
SyslogFacility string // .
|
||||||
|
TCPKeepAlive sshBool // .
|
||||||
|
UseDNS sshBool // .
|
||||||
|
UsePAM sshBool // .
|
||||||
|
VersionAddendum string // .
|
||||||
|
X11UseLocalhost sshBool // .
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListenAddr struct {
|
||||||
|
Addr string // hostname|address, hostname:port, IPv4_address:port, or [hostname|address]:port in conf string.
|
||||||
|
Port uint16
|
||||||
|
RDomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchSshd struct {
|
||||||
|
Criteria map[string]string
|
||||||
|
Rules []SshdMatchRule
|
||||||
|
}
|
10
config/type.go
Normal file
10
config/type.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type sshBool bool
|
||||||
|
|
||||||
|
func (b sshBool) Str() string {
|
||||||
|
if b {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
}
|
34
config/validator.go
Normal file
34
config/validator.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oleiade/reflections"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
e := fmt.Sprintf(
|
||||||
|
"field %v value %v is not allowed",
|
||||||
|
k, realV)
|
||||||
|
return false, errors.New(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
9
go.mod
9
go.mod
@ -1,9 +0,0 @@
|
|||||||
module r00t2.io/sshsecure
|
|
||||||
|
|
||||||
go 1.15
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a
|
|
||||||
github.com/go-restruct/restruct v1.2.0-alpha
|
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
|
||||||
)
|
|
24
go.sum
24
go.sum
@ -1,24 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
|
||||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
|
||||||
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
|
|
||||||
github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -27,7 +27,8 @@ import (
|
|||||||
// Misc
|
// Misc
|
||||||
const (
|
const (
|
||||||
// Where to find an up-to-date copy of the upstream moduli and its SHA3-512 (NIST) checksum.
|
// Where to find an up-to-date copy of the upstream moduli and its SHA3-512 (NIST) checksum.
|
||||||
pregenURL string = "https://anongit.mindrot.org/openssh.git/tree/moduli"
|
// pregenURL string = "https://anongit.mindrot.org/openssh.git/plain/moduli"
|
||||||
|
pregenURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/moduli"
|
||||||
// This is the best way I could think of to verify integrity, since the file itself doesn't have a signature or anything like that.
|
// This is the best way I could think of to verify integrity, since the file itself doesn't have a signature or anything like that.
|
||||||
pregenCksum string = "106EDB19A936608D065D2E8E81F7BDE7" +
|
pregenCksum string = "106EDB19A936608D065D2E8E81F7BDE7" +
|
||||||
"434AF80EF81102E9440B99ACB98FBEF8" +
|
"434AF80EF81102E9440B99ACB98FBEF8" +
|
||||||
@ -37,7 +38,8 @@ const (
|
|||||||
parseTag string = "sshmoduli"
|
parseTag string = "sshmoduli"
|
||||||
// The recommended minimum moduli to have available.
|
// The recommended minimum moduli to have available.
|
||||||
recMinMod int = 400
|
recMinMod int = 400
|
||||||
// The
|
// The minimum bits for filtering. It's generally bits - 1
|
||||||
|
minBits uint8 = 4095
|
||||||
)
|
)
|
||||||
|
|
||||||
// The header line on the /etc/ssh/moduli file.
|
// The header line on the /etc/ssh/moduli file.
|
||||||
@ -55,7 +57,8 @@ const (
|
|||||||
timeFormat string = "20060102150405" // %Y%m%d%H%M%S
|
timeFormat string = "20060102150405" // %Y%m%d%H%M%S
|
||||||
)
|
)
|
||||||
|
|
||||||
// For validation
|
// For validation. Currently unused.
|
||||||
|
/*
|
||||||
var (
|
var (
|
||||||
validTypes = []uint8{
|
validTypes = []uint8{
|
||||||
0, // Unknown, not tested
|
0, // Unknown, not tested
|
||||||
@ -69,3 +72,4 @@ var (
|
|||||||
0x04, // Probabilistic Miller-Rabin primality tests.
|
0x04, // Probabilistic Miller-Rabin primality tests.
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
*/
|
||||||
|
@ -28,33 +28,38 @@ import (
|
|||||||
"golang.org/x/crypto/sha3"
|
"golang.org/x/crypto/sha3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPregen() ([]byte, error) {
|
// getPregen gets the pregenerated moduli from upstream mirror.
|
||||||
|
func getPregen() (Moduli, error) {
|
||||||
|
m := Moduli{}
|
||||||
// get the pregenerated moduli
|
// get the pregenerated moduli
|
||||||
resp, err := http.Get(pregenURL)
|
resp, err := http.Get(pregenURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return m, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errors.New(fmt.Sprintf("returned status code %v: %v", resp.StatusCode, resp.Status))
|
return m, errors.New(fmt.Sprintf("returned status code %v: %v", resp.StatusCode, resp.Status))
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
b := make([]byte, resp.ContentLength)
|
b := make([]byte, resp.ContentLength)
|
||||||
if _, err = resp.Body.Read(b); err != nil {
|
if _, err = resp.Body.Read(b); err != nil {
|
||||||
return nil, err
|
return m, err
|
||||||
}
|
}
|
||||||
// and compare the SHA3-512 (NIST) checksum.
|
// and compare the SHA3-512 (NIST) checksum.
|
||||||
s := sha3.New512()
|
s := sha3.New512()
|
||||||
if _, err = s.Write(b); err != nil {
|
if _, err = s.Write(b); err != nil {
|
||||||
// TODO: return nil instead of b?
|
// TODO: return nil instead of b?
|
||||||
return b, err
|
return m, err
|
||||||
}
|
}
|
||||||
goodCksum, err := hex.DecodeString(pregenCksum)
|
goodCksum, err := hex.DecodeString(pregenCksum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return m, err
|
||||||
}
|
}
|
||||||
// We just compare the bytestrings.
|
// We just compare the bytestrings.
|
||||||
if bytes.Compare(s.Sum(nil), goodCksum) != 0 {
|
if bytes.Compare(s.Sum(nil), goodCksum) != 0 {
|
||||||
return nil, errors.New("checksums do not match")
|
return m, errors.New("checksums do not match")
|
||||||
}
|
}
|
||||||
return b, nil
|
if err := Unmarshal(b, m); err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
SSHSecure - a program to harden OpenSSH from defaults
|
|
||||||
Copyright (C) 2020 Brent Saner
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package moduli
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
}
|
|
@ -50,7 +50,7 @@ func (m *Moduli) Marshal() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// marshalEntry is used to parse a specific DH entry into the moduli.
|
// marshalEntry is used to parse a specific DH entry into the moduli.
|
||||||
func (m *ModuliEntry) marshalEntry() ([]byte, error) {
|
func (m *Entry) marshalEntry() ([]byte, error) {
|
||||||
mod := hex.EncodeToString(m.Modulus.Bytes())
|
mod := hex.EncodeToString(m.Modulus.Bytes())
|
||||||
s := fmt.Sprintf(
|
s := fmt.Sprintf(
|
||||||
"%v %v %v %v %v %v %v\n",
|
"%v %v %v %v %v %v %v\n",
|
||||||
@ -68,10 +68,10 @@ func (m *ModuliEntry) marshalEntry() ([]byte, error) {
|
|||||||
// Unmarshal writes the Moduli format into m from the /etc/ssh/moduli format in data.
|
// Unmarshal writes the Moduli format into m from the /etc/ssh/moduli format in data.
|
||||||
func Unmarshal(data []byte, m Moduli) error {
|
func Unmarshal(data []byte, m Moduli) error {
|
||||||
var lines []string
|
var lines []string
|
||||||
var entries []ModuliEntry
|
var entries []Entry
|
||||||
lines = strings.Split(string(data), "\n")
|
lines = strings.Split(string(data), "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
e := ModuliEntry{}
|
e := Entry{}
|
||||||
if reSkipLine.MatchString(line) {
|
if reSkipLine.MatchString(line) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -85,7 +85,8 @@ func Unmarshal(data []byte, m Moduli) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalEntry(line []string, m ModuliEntry) error {
|
func unmarshalEntry(line []string, m Entry) error {
|
||||||
|
var err error
|
||||||
if len(line) != 7 {
|
if len(line) != 7 {
|
||||||
return errors.New("field count mismatch")
|
return errors.New("field count mismatch")
|
||||||
}
|
}
|
||||||
@ -117,3 +118,19 @@ func unmarshalEntry(line []string, m ModuliEntry) error {
|
|||||||
m.Modulus.SetBytes(modb)
|
m.Modulus.SetBytes(modb)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Moduli) Harden() error {
|
||||||
|
var entries []Entry
|
||||||
|
for _, e := range m.Params {
|
||||||
|
if e.Size >= minBits {
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.Params = entries
|
||||||
|
if len(m.Params) < recMinMod {
|
||||||
|
return errors.New("does not meet recommended minimum moduli")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find way of testing/sieving primes
|
||||||
|
@ -26,11 +26,11 @@ import (
|
|||||||
// Moduli contains all data needed for generated /etc/ssh/moduli. of ModuliEntry entries.
|
// Moduli contains all data needed for generated /etc/ssh/moduli. of ModuliEntry entries.
|
||||||
type Moduli struct {
|
type Moduli struct {
|
||||||
Header string
|
Header string
|
||||||
Params []ModuliEntry
|
Params []Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moduli is a struct reflecting the format of a single /etc/ssh/moduli entry. See moduli(5) for details.
|
// Moduli is a struct reflecting the format of a single /etc/ssh/moduli entry. See moduli(5) for details.
|
||||||
type ModuliEntry struct {
|
type Entry struct {
|
||||||
Time time.Time // YYYYMMDDHHSS
|
Time time.Time // YYYYMMDDHHSS
|
||||||
/*
|
/*
|
||||||
// man 5 moduli:
|
// man 5 moduli:
|
||||||
|
@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package sharedconsts
|
package sharedconsts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -11,3 +29,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var IDCmnt = string(fmt.Sprintf("Autogenerated by %v (%v)", projName, projUrl))
|
var IDCmnt = string(fmt.Sprintf("Autogenerated by %v (%v)", projName, projUrl))
|
||||||
|
|
||||||
|
// Common file/directory paths.
|
||||||
|
const (
|
||||||
|
// System-wide files.
|
||||||
|
LockFile string = "/tmp/.sshsecure.lck"
|
||||||
|
SysSshConfDir string = "/etc/ssh"
|
||||||
|
// The following are joined with SysSshConfDir.
|
||||||
|
SshdConfFile string = "sshd_config"
|
||||||
|
SshConfFile string = "ssh_config"
|
||||||
|
HostEd25519File string = "ssh_host_ed25519_key"
|
||||||
|
HostRsaFile string = "ssh_host_rsa_key"
|
||||||
|
ModuliFile string = "moduli"
|
||||||
|
// Invoking user's files.
|
||||||
|
UserSshConfDir string = "~/.ssh"
|
||||||
|
// The following are joined with userSshConfDir.
|
||||||
|
UserSshConfFile string = "config"
|
||||||
|
UserEd25519File string = "id_ed25519"
|
||||||
|
UserRsaFile string = "id_rsa"
|
||||||
|
)
|
||||||
|
@ -43,7 +43,7 @@ const (
|
|||||||
CipherAes256Ctr string = "aes256-ctr"
|
CipherAes256Ctr string = "aes256-ctr"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allowed_ciphers = [...]string{CipherNull, CipherAes256Ctr}
|
var allowedCiphers = [...]string{CipherNull, CipherAes256Ctr}
|
||||||
|
|
||||||
// Key types.
|
// Key types.
|
||||||
const (
|
const (
|
||||||
@ -51,7 +51,7 @@ const (
|
|||||||
KeyRsa string = "ssh-rsa"
|
KeyRsa string = "ssh-rsa"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allowed_keytypes = [...]string{KeyEd25519, KeyRsa}
|
var allowedKeytypes = [...]string{KeyEd25519, KeyRsa}
|
||||||
|
|
||||||
// KDF names. I believe only bcrypt is supported upstream currently.
|
// KDF names. I believe only bcrypt is supported upstream currently.
|
||||||
const (
|
const (
|
||||||
@ -59,7 +59,7 @@ const (
|
|||||||
KdfBcrypt string = "bcrypt"
|
KdfBcrypt string = "bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allowed_kdfnames = [...]string{KdfNull, KdfBcrypt}
|
var allowedKdfnames = [...]string{KdfNull, KdfBcrypt}
|
||||||
|
|
||||||
// Key lengths.
|
// Key lengths.
|
||||||
const (
|
const (
|
||||||
|
@ -1,12 +1,30 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package sshkeys
|
package sshkeys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
`crypto/aes`
|
"crypto/aes"
|
||||||
`crypto/cipher`
|
"crypto/cipher"
|
||||||
`errors`
|
"errors"
|
||||||
|
|
||||||
// `golang.org/x/crypto/ssh/internal/bcrypt_pbkdf` Golang doesn't let you import "internal" libs. Fine. Lame language.
|
// `golang.org/x/crypto/ssh/internal/bcrypt_pbkdf` Golang doesn't let you import "internal" libs. Fine. Lame language.
|
||||||
`github.com/dchest/bcrypt_pbkdf`
|
"github.com/dchest/bcrypt_pbkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (k *EncryptedSSHKeyV1) setCrypt() error {
|
func (k *EncryptedSSHKeyV1) setCrypt() error {
|
||||||
@ -22,6 +40,7 @@ func (k *EncryptedSSHKeyV1) setCrypt() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *EncryptedSSHKeyV1) setCipher() error {
|
func (k *EncryptedSSHKeyV1) setCipher() error {
|
||||||
|
var err error
|
||||||
switch k.CipherName {
|
switch k.CipherName {
|
||||||
case CipherAes256Ctr:
|
case CipherAes256Ctr:
|
||||||
if k.Crypt.Cipher, err = aes.NewCipher(k.Crypt.PrivateKey); err != nil {
|
if k.Crypt.Cipher, err = aes.NewCipher(k.Crypt.PrivateKey); err != nil {
|
||||||
@ -37,14 +56,15 @@ func (k *EncryptedSSHKeyV1) setCipher() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *EncryptedSSHKeyV1) setKDF() error {
|
func (k *EncryptedSSHKeyV1) setKDF() error {
|
||||||
|
var err error
|
||||||
// Upstream only currently supports bcrypt_pbkdf ("bcrypt").
|
// Upstream only currently supports bcrypt_pbkdf ("bcrypt").
|
||||||
// This should always eval to true, but is here for future planning in case other KDF are implemented.
|
// This should always eval to true, but is here for future planning in case other KDF are implemented.
|
||||||
switch k.KDFName {
|
switch k.KDFName {
|
||||||
case KdfBcrypt:
|
case KdfBcrypt:
|
||||||
if k.Crypt.CryptKey, err = bcrypt_pbkdf.Key(k.Passphrase,
|
if k.Crypt.CryptKey, err = bcrypt_pbkdf.Key(k.Passphrase,
|
||||||
k.KDFOpts.Salt,
|
k.KDFOpts.Salt,
|
||||||
int(k.KDFOpts.Rounds),
|
int(k.KDFOpts.Rounds),
|
||||||
kdfKeyLen + len(k.KDFOpts.Salt),
|
kdfKeyLen+len(k.KDFOpts.Salt),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
@ -54,4 +74,5 @@ func (k *EncryptedSSHKeyV1) setKDF() error {
|
|||||||
default:
|
default:
|
||||||
return errors.New("could not find KDF")
|
return errors.New("could not find KDF")
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
@ -22,7 +22,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"r00t2.io/sshsecure/sharedconsts"
|
"r00t2.io/sshsecure/sharedconsts"
|
||||||
)
|
)
|
||||||
@ -34,19 +33,19 @@ func (k *EncryptedSSHKeyV1) validate() error {
|
|||||||
var validCipher bool
|
var validCipher bool
|
||||||
var validKDF bool
|
var validKDF bool
|
||||||
var validKT bool
|
var validKT bool
|
||||||
for _, v := range allowed_ciphers {
|
for _, v := range allowedCiphers {
|
||||||
if v == k.CipherName {
|
if v == k.CipherName {
|
||||||
validCipher = true
|
validCipher = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, v := range allowed_kdfnames {
|
for _, v := range allowedKdfnames {
|
||||||
if v == k.KDFName {
|
if v == k.KDFName {
|
||||||
validKDF = true
|
validKDF = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, v := range allowed_keytypes {
|
for _, v := range allowedKeytypes {
|
||||||
if v == k.DefKeyType {
|
if v == k.DefKeyType {
|
||||||
validKT = true
|
validKT = true
|
||||||
}
|
}
|
||||||
@ -107,7 +106,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error {
|
|||||||
return errors.New("unknown key type; could not generate private/public keypair")
|
return errors.New("unknown key type; could not generate private/public keypair")
|
||||||
}
|
}
|
||||||
k.Keys = append(k.Keys, pk)
|
k.Keys = append(k.Keys, pk)
|
||||||
// We also need an encrypter/decrypter since this is an encrypted key.
|
// We also need an encryptor/decryptor since this is an encrypted key.
|
||||||
if err := k.setCrypt(); err != nil {
|
if err := k.setCrypt(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -120,7 +119,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error {
|
|||||||
|
|
||||||
func (k *SSHKeyV1) validate() error {
|
func (k *SSHKeyV1) validate() error {
|
||||||
var validKT bool
|
var validKT bool
|
||||||
for _, v := range allowed_keytypes {
|
for _, v := range allowedKeytypes {
|
||||||
if v == k.DefKeyType {
|
if v == k.DefKeyType {
|
||||||
validKT = true
|
validKT = true
|
||||||
}
|
}
|
||||||
@ -143,7 +142,7 @@ func (k *SSHKeyV1) Generate(force bool) error {
|
|||||||
// Currently, OpenSSH has an option for multiple private keys. However, it is hardcoded to 1.
|
// Currently, OpenSSH has an option for multiple private keys. However, it is hardcoded to 1.
|
||||||
// If multiple key support is added in the future, will need to re-tool how I do this, perhaps, in the future. TODO.
|
// If multiple key support is added in the future, will need to re-tool how I do this, perhaps, in the future. TODO.
|
||||||
pk := SSHPrivKey{
|
pk := SSHPrivKey{
|
||||||
Comment: fmt.Sprintf("Autogenerated via SSHSecure (%v)", projUrl),
|
Comment: sharedconsts.IDCmnt,
|
||||||
}
|
}
|
||||||
pk.Checksum = make([]byte, 4)
|
pk.Checksum = make([]byte, 4)
|
||||||
if _, err := rand.Read(pk.Checksum); err != nil {
|
if _, err := rand.Read(pk.Checksum); err != nil {
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
SSHSecure - a program to harden OpenSSH from defaults
|
|
||||||
Copyright (C) 2020 Brent Saner
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sshkeys
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
}
|
|
172
utils/files.go
Normal file
172
utils/files.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
SSHSecure - a program to harden OpenSSH from defaults
|
||||||
|
Copyright (C) 2020 Brent Saner
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
gopath "path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"r00t2.io/sysutils/paths"
|
||||||
|
|
||||||
|
"r00t2.io/sshsecure/sharedconsts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func backupWrite(path string, data []byte, system bool, force bool) error {
|
||||||
|
var err error
|
||||||
|
var uid int
|
||||||
|
var gid int
|
||||||
|
var running bool
|
||||||
|
var runningInfo runInfo
|
||||||
|
var curInfo runInfo
|
||||||
|
uid, gid, err = getPerms(system)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = paths.RealPath(&path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
curInfo = getInfo()
|
||||||
|
parent := filepath.Dir(path)
|
||||||
|
parentExists, err := paths.RealPathExists(&parent)
|
||||||
|
infoFile := gopath.Join(
|
||||||
|
parent,
|
||||||
|
fmt.Sprintf(".%v.gob",
|
||||||
|
filepath.Base(path),
|
||||||
|
))
|
||||||
|
if err = paths.MakeDirIfNotExist(&parent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
exists, err := paths.RealPathExists(&path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !parentExists {
|
||||||
|
// It exists *now*, but it's a new dir. We set perms thusly.
|
||||||
|
os.Chmod(parent, 0700)
|
||||||
|
}
|
||||||
|
if gobExists, err := paths.RealPathExists(&infoFile); err != nil {
|
||||||
|
return err
|
||||||
|
} else if gobExists && !force {
|
||||||
|
return nil // Don't go any further; the file was already generated.
|
||||||
|
}
|
||||||
|
running, runningInfo, err = isRunning()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists && running {
|
||||||
|
e := fmt.Sprintf(
|
||||||
|
"refusing to write to %v; another instance of program is currently running with PID %v started at %v",
|
||||||
|
path, runningInfo.Pid, runningInfo.TimeStarted)
|
||||||
|
return errors.New(e)
|
||||||
|
} else if exists && !running {
|
||||||
|
newName := fmt.Sprintf("%v.bak.%d", path, curInfo.TimeStarted.Unix())
|
||||||
|
if err = os.Rename(path, newName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FINALLY. The business end.
|
||||||
|
w, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gobFile, err := os.Create(infoFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g := gob.NewEncoder(gobFile)
|
||||||
|
defer w.Close()
|
||||||
|
defer gobFile.Close()
|
||||||
|
if err = w.Chmod(0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = w.Chown(uid, gid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = w.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = gobFile.Chmod(0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = gobFile.Chown(uid, gid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = g.Encode(curInfo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInfo() runInfo {
|
||||||
|
curInfo := runInfo{
|
||||||
|
TimeStarted: getTime(),
|
||||||
|
Pid: getPid(),
|
||||||
|
}
|
||||||
|
return curInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPid() int {
|
||||||
|
return os.Getpid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPerms(system bool) (int, int, error) {
|
||||||
|
var uid int
|
||||||
|
var gid int
|
||||||
|
if system {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
return 0, 0, errors.New("cannot create system file without root permissions")
|
||||||
|
}
|
||||||
|
uid = 0
|
||||||
|
gid = 0
|
||||||
|
} else {
|
||||||
|
uid = os.Geteuid()
|
||||||
|
gid = os.Getegid()
|
||||||
|
}
|
||||||
|
return uid, gid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTime() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRunning() (bool, runInfo, error) {
|
||||||
|
curInfo := getInfo()
|
||||||
|
var fileInfo runInfo
|
||||||
|
lckFile := sharedconsts.LockFile
|
||||||
|
exists, _ := paths.RealPathExists(&lckFile)
|
||||||
|
if !exists {
|
||||||
|
// This should never happen. If it does, I goofed the locking (or it's not invoked from a shell).
|
||||||
|
return false, fileInfo, errors.New("*grabs shotgun* computer's haunted")
|
||||||
|
}
|
||||||
|
reader, err := os.Open(lckFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, fileInfo, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
gobDecoder := gob.NewDecoder(reader)
|
||||||
|
gobDecoder.Decode(&fileInfo)
|
||||||
|
if fileInfo != curInfo {
|
||||||
|
return true, fileInfo, nil
|
||||||
|
}
|
||||||
|
return false, fileInfo, nil
|
||||||
|
}
|
10
utils/struct.go
Normal file
10
utils/struct.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runInfo struct {
|
||||||
|
TimeStarted time.Time
|
||||||
|
Pid int
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user