mirror of
https://github.com/moul/sshportal.git
synced 2025-09-09 22:24:29 +08:00
Add option to encrypt sensitive data
This commit is contained in:
parent
01d464f4c5
commit
571b37da6b
7 changed files with 175 additions and 8 deletions
|
@ -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)
|
||||
|
||||
|
|
3
Makefile
3
Makefile
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
83
crypto.go
83
crypto.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
13
main.go
|
@ -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 {
|
||||
|
|
67
shell.go
67
shell.go
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue