├── utils ├── crypto.go └── ssh.go ├── sshkeys.go ├── config └── config.go └── README.md /utils/crypto.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type AuthorizedKey struct { 11 | User string 12 | Account string 13 | Host string 14 | } 15 | 16 | func readKeyFile(keyspath string, user string) (string, error) { 17 | keyname := filepath.Join(keyspath, user+".pub") 18 | contents, err := ioutil.ReadFile(keyname) 19 | return string(contents), err 20 | } 21 | 22 | func createAuthorizedKeysFile(keysPath string, keys []AuthorizedKey) (string, error) { 23 | file, err := ioutil.TempFile("", "auth") 24 | if err != nil { 25 | return "", err 26 | } 27 | defer file.Close() 28 | 29 | for _, key := range keys { 30 | log.Printf("Adding key for \"%s\" to \"%s@%s\"\n", key.User, key.Account, key.Host) 31 | keyContents, err := readKeyFile(keysPath, key.User) 32 | if err != nil { 33 | defer os.Remove(file.Name()) 34 | return "", err 35 | } 36 | if _, err = file.WriteString(keyContents + "\n"); err != nil { 37 | defer os.Remove(file.Name()) 38 | return "", err 39 | } 40 | } 41 | 42 | return file.Name(), nil 43 | } 44 | -------------------------------------------------------------------------------- /sshkeys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/dpecos/sshkeys/config" 8 | "github.com/dpecos/sshkeys/utils" 9 | 10 | kingpin "gopkg.in/alecthomas/kingpin.v2" 11 | ) 12 | 13 | var ( 14 | app = kingpin.New("sshkeys", "Deploy SSH keys to remote hosts") 15 | configFile = app.Flag("config", "Configuration file specifying hosts, grants and ACLs").Required().String() 16 | privateKey = app.Flag("privateKey", "Private key to use to upload authorized_keys file to remote hosts").Required().String() 17 | keyring = app.Flag("keyring", "Path to a folder containing all user public keys (each one as a separate file named like user.pub)").Required().String() 18 | deploy = app.Command("deploy", "Generate and upload an authorized_keys file to remote hosts defined in configuation file") 19 | ) 20 | 21 | func main() { 22 | 23 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 24 | 25 | case deploy.FullCommand(): 26 | hosts, acls, err := config.LoadConfig(*configFile) 27 | if err != nil { 28 | log.Panic(err) 29 | } 30 | 31 | if err := utils.UploadKeys(*privateKey, *keyring, hosts, acls); err != nil { 32 | log.Fatalf("ERROR: %s\n", err) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /utils/ssh.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | config "github.com/dpecos/sshkeys/config" 8 | 9 | scp "github.com/bramvdbogaerde/go-scp" 10 | "github.com/bramvdbogaerde/go-scp/auth" 11 | ) 12 | 13 | func uploadFileToHost(privateKey string, host config.Host, file string, remoteFilename string) error { 14 | clientConfig, _ := auth.PrivateKey(host.Account, privateKey) 15 | client := scp.NewClient(host.Host+":22", &clientConfig) 16 | 17 | err := client.Connect() 18 | if err != nil { 19 | log.Fatal("Couldn't establisc a connection to the remote server: ", err) 20 | return err 21 | } 22 | defer client.Session.Close() 23 | 24 | reader, _ := os.Open(file) 25 | defer reader.Close() 26 | 27 | client.CopyFile(reader, remoteFilename, "0644") 28 | 29 | return nil 30 | } 31 | 32 | func putKeysInHost(privateKey string, keysPath string, keys []AuthorizedKey, host config.Host) error { 33 | file, err := createAuthorizedKeysFile(keysPath, keys) 34 | if err != nil { 35 | return err 36 | } 37 | defer os.Remove(file) 38 | 39 | err = uploadFileToHost(privateKey, host, file, "~/.ssh/authorized_keys") 40 | 41 | return err 42 | } 43 | 44 | func UploadKeys(privateKey, keysPath string, hosts []config.Host, acls map[string]config.ACL) error { 45 | for _, host := range hosts { 46 | var keys []AuthorizedKey 47 | keysAdded := make(map[string]bool) 48 | 49 | for _, user := range host.Users { 50 | if _, seen := keysAdded[user+host.Account+host.Host]; !seen { 51 | keysAdded[user+host.Account+host.Host] = true 52 | keys = append(keys, AuthorizedKey{user, host.Account, host.Host}) 53 | } 54 | } 55 | for _, acl := range host.ACLs { 56 | for _, user := range acl.Users { 57 | if _, seen := keysAdded[user+host.Account+host.Host]; !seen { 58 | keysAdded[user+host.Account+host.Host] = true 59 | keys = append(keys, AuthorizedKey{user, host.Account, host.Host}) 60 | } 61 | } 62 | } 63 | 64 | if len(keys) > 0 { 65 | log.Printf("Uploading keys to %s@%s...\n", host.Account, host.Host) 66 | err := putKeysInHost(privateKey, keysPath, keys, host) 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "log" 8 | 9 | ini "gopkg.in/ini.v1" 10 | ) 11 | 12 | type Host struct { 13 | ID string 14 | Host string 15 | Account string 16 | ACLs []ACL 17 | Users []string 18 | } 19 | 20 | type ACL struct { 21 | Name string 22 | Users []string 23 | } 24 | 25 | func loadACLs(cfg *ini.File) (map[string]ACL, error) { 26 | acls := make(map[string]ACL) 27 | section, err := cfg.GetSection("ACLs") 28 | if err != nil { 29 | return nil, fmt.Errorf("Section ACLs not found in config file: %s", err) 30 | } 31 | for _, key := range section.Keys() { 32 | acl := new(ACL) 33 | acl.Name = key.Name() 34 | acl.Users = key.Strings(",") 35 | acls[acl.Name] = *acl 36 | } 37 | return acls, nil 38 | } 39 | 40 | func getKey(section *ini.Section, key string) *ini.Key { 41 | val, err := section.GetKey(key) 42 | if err != nil { 43 | log.Fatalf("Key %s not found", key) 44 | } 45 | return val 46 | } 47 | 48 | func loadHosts(cfg *ini.File, acls map[string]ACL) ([]Host, error) { 49 | var hosts []Host 50 | section, err := cfg.GetSection("Hosts") 51 | if err != nil { 52 | return nil, fmt.Errorf("Section Hosts not found in config file: %s", err) 53 | } 54 | for _, key := range section.Keys() { 55 | if strings.HasSuffix(key.Name(), ".host") { 56 | id := strings.Split(key.Name(), ".")[0] 57 | host := new(Host) 58 | host.ID = id 59 | host.Host = getKey(section, id+".host").String() 60 | host.Account = getKey(section, id+".account").String() 61 | if section.HasKey(id + ".users") { 62 | host.Users = getKey(section, id+".users").Strings(",") 63 | } 64 | if section.HasKey(id + ".acls") { 65 | aclNames := getKey(section, id+".acls").Strings(",") 66 | for _, name := range aclNames { 67 | host.ACLs = append(host.ACLs, acls[name]) 68 | } 69 | } 70 | hosts = append(hosts, *host) 71 | } 72 | } 73 | return hosts, nil 74 | } 75 | 76 | func LoadConfig(f string) ([]Host, map[string]ACL, error) { 77 | cfg, err := ini.Load(f) 78 | if err != nil { 79 | return nil, nil, fmt.Errorf("Could not open config file %s: %s", f, err) 80 | } 81 | 82 | acls, err := loadACLs(cfg) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | hosts, err := loadHosts(cfg, acls) 88 | 89 | return hosts, acls, nil 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple access control list for your servers infraestructure using just SSH keys. 2 | 3 | About 4 | ===== 5 | 6 | With sshkeys it's really easy to define your hosts list and who can access each of them. You can specify a list of users or use ACLs (named user lists) so a change in a team would be as easy as modifying that ACL and redeploying the SSH keys. 7 | 8 | sshkeys works rewriting ~/.ssh/authorized_keys file of the remote hosts, adding only the required public keys ot the users allowed to access that host. 9 | 10 | Important: in order to properly deploy to remote hosts, SSH access is required. The user executing sshkey must have access to remote hosts (with the account specified in the config file) in order to deploy the authorized keys. Make sure current user is always included in every deployment. 11 | 12 | Installation 13 | ============ 14 | 15 | Golang is required to build sshkeys. 16 | 17 | ``` 18 | $ go get github.com/dpecos/sshkeys 19 | $ go build github.com/dpecos/sshkeys 20 | ``` 21 | 22 | Usage 23 | ===== 24 | * CONFIG is the path to the YML to use for SSH keys deployment 25 | * PRIVATEKEY is the path to the private key to use for the hosts' account (the same for all of them) 26 | * KEYRING is the path to the directory containing the users SSH public files, named as username.pub, where username is the name of the user specified in the config file 27 | 28 | ``` 29 | $ sshkeys --help 30 | usage: sshkeys --config=CONFIG --privateKey=PRIVATEKEY --keyring=KEYRING [] [ ...] 31 | 32 | Deploy SSH keys to remote hosts 33 | 34 | Flags: 35 | --help Show context-sensitive help (also try --help-long and --help-man). 36 | --config=CONFIG Configuration file specifying hosts, grants and ACLs 37 | --privateKey=PRIVATEKEY Private key to use to upload authorized_keys file to remote hosts 38 | --keyring=KEYRING Path to a folder containing all user public keys (each one as a separate file named like 39 | user.pub) 40 | 41 | Commands: 42 | help [...] 43 | Show help. 44 | 45 | deploy 46 | Generate and upload an authorized_keys file to remote hosts defined in configuation file 47 | ``` 48 | 49 | Example config file 50 | =================== 51 | 52 | [Hosts] 53 | server1.host = server1.test.com 54 | server1.account = root 55 | server1.acls = devops, developers 56 | server1.users = dpecos, john 57 | 58 | gw.host = gw.test.com 59 | gw.account = user 60 | gw.acls = devops 61 | 62 | private.host = priv.test.com 63 | private.account = dpecos 64 | private.users = dpecos 65 | 66 | [ACLs] 67 | devops = john 68 | developers = james, una 69 | --------------------------------------------------------------------------------