Add option to encrypt sensitive data

This commit is contained in:
Manfred Touron 2017-11-24 14:29:41 +01:00
parent 01d464f4c5
commit 571b37da6b
7 changed files with 175 additions and 8 deletions

View file

@ -4,6 +4,7 @@
* Add 'key setup' command (easy SSH key installation)
* Add Updated and Created fields in 'ls' commands
* Add `--aes-key` option to encrypt sensitive data
## v1.3.0 (2017-11-23)

View file

@ -4,6 +4,7 @@ GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS ?= -X main.GIT_SHA=$(GIT_SHA) -X main.GIT_TAG=$(GIT_TAG) -X main.GIT_BRANCH=$(GIT_BRANCH)
VERSION ?= $(shell grep 'VERSION =' main.go | cut -d'"' -f2)
PORT ?= 2222
AES_KEY ?= my-dummy-aes-key
.PHONY: install
install:
@ -24,7 +25,7 @@ _docker_install:
.PHONY: dev
dev:
-go get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug --bind-address=:$(PORT)" .
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY)" .
.PHONY: test
test:

View file

@ -34,6 +34,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
* User Roles
* User invitations
* Easy authorized_keys installation
* Sensitive data encryption
## Usage
@ -140,13 +141,13 @@ acl update [-h] [--comment=<value>] [--action=<value>] [--weight=<value>] [--ass
# config management
config help
config backup [-h] [--indent]
config restore [-h] [--confirm]
config backup [-h] [--indent] [--decrypt]
config restore [-h] [--confirm] [--decrypt]
# host management
host help
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] <username>[:<password>]@<host>[:<port>]
host inspect [-h] HOST...
host inspect [-h] [--decrypt] HOST...
host ls [-h]
host rm [-h] HOST...
host update [-h] [--name=<value>] [--comment=<value>] [--fingerprint=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] HOST...
@ -161,7 +162,7 @@ hostgroup rm [-h] HOSTGROUP...
# key management
key help
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
key inspect [-h] KEY...
key inspect [-h] [--decrypt] KEY...
key ls [-h]
key rm [-h] KEY...
key setup [-h] KEY

View file

@ -2,11 +2,15 @@ package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"strings"
gossh "golang.org/x/crypto/ssh"
@ -47,3 +51,82 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
return &key, nil
}
func encrypt(key []byte, text string) (string, error) {
plaintext := []byte(text)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return base64.URLEncoding.EncodeToString(ciphertext), nil
}
func decrypt(key []byte, cryptoText string) (string, error) {
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", fmt.Errorf("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return fmt.Sprintf("%s", ciphertext), nil
}
func safeDecrypt(key []byte, cryptoText string) string {
if len(key) == 0 {
return cryptoText
}
out, err := decrypt(key, cryptoText)
if err != nil {
return cryptoText
}
return out
}
func HostEncrypt(aesKey string, host *Host) error {
if aesKey == "" {
return nil
}
var err error
if host.Password != "" {
if host.Password, err = encrypt([]byte(aesKey), host.Password); err != nil {
return err
}
}
return nil
}
func HostDecrypt(aesKey string, host *Host) {
if aesKey == "" {
return
}
if host.Password != "" {
host.Password = safeDecrypt([]byte(aesKey), host.Password)
}
}
func SSHKeyEncrypt(aesKey string, key *SSHKey) error {
if aesKey == "" {
return nil
}
var err error
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
return err
}
func SSHKeyDecrypt(aesKey string, key *SSHKey) {
if aesKey == "" {
return
}
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
}

View file

@ -76,7 +76,12 @@ xssh -l admin host ls
xssh -l admin config backup --indent > backup-1
xssh -l admin config restore --confirm < backup-1
xssh -l admin config backup --indent > backup-2
diff <(cat backup-1 | grep -v '"date":') <(cat backup-2 | grep -v '"date":')
(
cat backup-1 | grep -v '"date":' > backup-1.clean
cat backup-2 | grep -v '"date":' > backup-2.clean
set -xe
diff backup-1.clean backup-2.clean
)
# post cleanup
#cleanup

13
main.go
View file

@ -75,6 +75,10 @@ func main() {
Usage: "SSH user that spawns a configuration shell",
Value: "admin",
},
cli.StringFlag{
Name: "aes-key",
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
},
}
app.Action = server
if err := app.Run(os.Args); err != nil {
@ -83,6 +87,11 @@ func main() {
}
func server(c *cli.Context) error {
switch len(c.String("aes-key")) {
case 0, 16, 24, 32:
default:
return fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
// db
db, err := gorm.Open("sqlite3", c.String("db-conn"))
if err != nil {
@ -152,6 +161,10 @@ func server(c *cli.Context) error {
return
}
// decrypt key and password
HostDecrypt(c.String("aes-key"), host)
SSHKeyDecrypt(c.String("aes-key"), host.SSHKey)
switch action {
case "allow":
if err := proxy(s, host); err != nil {

View file

@ -277,6 +277,7 @@ GLOBAL OPTIONS:
Usage: "Dumps a backup",
Flags: []cli.Flag{
cli.BoolFlag{Name: "indent", Usage: "uses indented JSON"},
cli.BoolFlag{Name: "decrypt", Usage: "decrypt sensitive data"},
},
Description: "ssh admin@portal config backup > sshportal.bkp",
Action: func(c *cli.Context) error {
@ -288,12 +289,35 @@ GLOBAL OPTIONS:
if err := db.Find(&config.Hosts).Error; err != nil {
return err
}
if err := db.Find(&config.SSHKeys).Error; err != nil {
return err
}
for _, key := range config.SSHKeys {
SSHKeyDecrypt(globalContext.String("aes-key"), key)
}
if !c.Bool("decrypt") {
for _, key := range config.SSHKeys {
if err := SSHKeyEncrypt(globalContext.String("aes-key"), key); err != nil {
return err
}
}
}
if err := db.Find(&config.Hosts).Error; err != nil {
return err
}
for _, host := range config.Hosts {
HostDecrypt(globalContext.String("aes-key"), host)
}
if !c.Bool("decrypt") {
for _, host := range config.Hosts {
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
return err
}
}
}
if err := db.Find(&config.UserKeys).Error; err != nil {
return err
}
@ -325,6 +349,7 @@ GLOBAL OPTIONS:
Description: "ssh admin@portal config restore < sshportal.bkp",
Flags: []cli.Flag{
cli.BoolFlag{Name: "confirm", Usage: "yes, I want to replace everything with this backup!"},
cli.BoolFlag{Name: "decrypt", Usage: "do not encrypt sensitive data"},
},
Action: func(c *cli.Context) error {
if err := UserCheckRoles(myself, []string{"admin"}); err != nil {
@ -363,6 +388,11 @@ GLOBAL OPTIONS:
}
}
for _, host := range config.Hosts {
if !c.Bool("decrypt") {
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
return err
}
}
if err := tx.Create(&host).Error; err != nil {
tx.Rollback()
return err
@ -393,6 +423,11 @@ GLOBAL OPTIONS:
}
}
for _, sshKey := range config.SSHKeys {
if !c.Bool("decrypt") {
if err := SSHKeyEncrypt(globalContext.String("aes-key"), sshKey); err != nil {
return err
}
}
if err := tx.Create(&sshKey).Error; err != nil {
tx.Rollback()
return err
@ -487,6 +522,11 @@ GLOBAL OPTIONS:
return err
}
// encrypt
if err := HostEncrypt(globalContext.String("aes-key"), host); err != nil {
return err
}
if err := db.Create(&host).Error; err != nil {
return err
}
@ -497,6 +537,9 @@ GLOBAL OPTIONS:
Name: "inspect",
Usage: "Shows detailed information on one or more hosts",
ArgsUsage: "HOST...",
Flags: []cli.Flag{
cli.BoolFlag{Name: "decrypt", Usage: "Decrypt sensitive data"},
},
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
@ -506,7 +549,7 @@ GLOBAL OPTIONS:
return err
}
var hosts []Host
var hosts []*Host
db = db.Preload("Groups")
if UserHasRole(myself, "admin") {
db = db.Preload("SSHKey")
@ -515,6 +558,12 @@ GLOBAL OPTIONS:
return err
}
if c.Bool("decrypt") {
for _, host := range hosts {
HostDecrypt(globalContext.String("aes-keuy"), host)
}
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(hosts)
@ -822,6 +871,11 @@ GLOBAL OPTIONS:
}
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
if globalContext.String("aes-key") != "" {
if err := SSHKeyEncrypt(globalContext.String("aes-key"), key); err != nil {
return err
}
}
if err != nil {
return err
}
@ -844,6 +898,9 @@ GLOBAL OPTIONS:
Name: "inspect",
Usage: "Shows detailed information on one or more keys",
ArgsUsage: "KEY...",
Flags: []cli.Flag{
cli.BoolFlag{Name: "decrypt", Usage: "Decrypt sensitive data"},
},
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
@ -853,11 +910,17 @@ GLOBAL OPTIONS:
return err
}
var keys []SSHKey
var keys []*SSHKey
if err := SSHKeysByIdentifiers(db, c.Args()).Find(&keys).Error; err != nil {
return err
}
if c.Bool("decrypt") {
for _, key := range keys {
SSHKeyDecrypt(globalContext.String("aes-key"), key)
}
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(keys)