package main /* Signs all host keys. */ import ( "bufio" "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "strconv" "strings" "syscall" "time" "github.com/urfave/cli" ) // Options available via a signing request. type httpSignRequest struct { Environment string APIKey string Type string KeyID string ValidPrincipals []string Options map[string]string Extensions map[string]string PublicKeys []string Duration time.Duration } // The standard API responses. type httpSignResponse struct { Successful bool Message string SignedKeys []string } // Flags for the sign command. func signFlags() []cli.Flag { return []cli.Flag{ cli.StringFlag{Name: "key, k"}, } } // The sign command calls this function. func sign(c *cli.Context) error { // Load the configuration. config := initConfig(c) // If we want to just sign a single key, pass it via the argument. hostKey := c.String("key") // All host key files. var hostKeys []string // If a host key file was provided, we just use it. Otherwise we find system host keys. if hostKey != "" { hostKeys = append(hostKeys, hostKey) } else { // Host keys are stored in /etc/ssh/. files, err := ioutil.ReadDir("/etc/ssh/") if err != nil { return err } // Host keys end in .pub, but are not certificates. for _, f := range files { if strings.Contains(f.Name(), ".pub") && !strings.Contains(f.Name(), "-cert") && f.Name() != "ssh_host_key.pub" { hostKeys = append(hostKeys, "/etc/ssh/"+f.Name()) } } } // Setup a signing request. sr := httpSignRequest{} sr.Environment = config.SignOptions.Environment sr.APIKey = config.SignOptions.APIKey sr.Type = "host" sr.KeyID = config.SignOptions.KeyID sr.Duration = config.SignOptions.Duration // If a key ID is not set in the configuration, we will use the hostname. if sr.KeyID == "" { sr.KeyID, _ = os.Hostname() } // Read the host keys into the signing request. for _, hostKey := range hostKeys { // Read the host public key. pubKeyFile, err := ioutil.ReadFile(hostKey) if err != nil { return fmt.Errorf("Unable to read public key: %v", err) } sr.PublicKeys = append(sr.PublicKeys, string(pubKeyFile)) } // Convert signing request into JSON for the request. srData, err := json.Marshal(sr) if err != nil { return err } // Setup request. req, _ := http.NewRequest("POST", config.CertServer+"/sign", bytes.NewBuffer(srData)) req.Header.Add("content-type", "application/json") // Send the request. res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() // Parse the response. var signResponse httpSignResponse decoder := json.NewDecoder(res.Body) err = decoder.Decode(&signResponse) if err != nil { return err } // If successful, we need to pull the signed keys and store them/update the sshd configurations. if signResponse.Successful { // The configuration file path. sshdConfig := "/etc/ssh/sshd_config" // Open the sshd_config file. sshdConfigFile, err := os.Open(sshdConfig) if err != nil { return err } defer sshdConfigFile.Close() configReader := bufio.NewReader(sshdConfigFile) // We save our changes to the _new configuration file temporarily during edits. newConfig, err := os.Create(sshdConfig + "_new") if err != nil { return err } // We go through the file until all host key configurations are found. // Once found, we then insert our certificate configurations. foundHostKeys := false // This is the last line read that is not a host key configuration. // We need to write this line after adding the certificate configurations. var lastReadLine string for { // Read a line. line, err := configReader.ReadString('\n') // If end of line, we are done reading. if err == io.EOF { break } // If error, something is wrong. if err != nil { return err } // Configurations in sshd_config is white space separated. If a whitepsace is not found, it is not a configuration line. i := strings.IndexByte(line, ' ') if i != -1 { // We pull the configuration name. conf := line[:i] // If we find a Match configuration, we want to stop here as anything below this line is specific to the match. if conf == "Match" { lastReadLine = line break } // If we found the host keys already, we check to see if this line is another host key or host certificate. // If it is not, we are done reading at this point and we need to store the line for writing after we isnert our config. if foundHostKeys && conf != "HostKey" && conf != "HostCertificate" { lastReadLine = line break } // If this is a host certificate configuration, we need to ignore it. if conf == "HostCertificate" { continue } // If this is a host key configuration, we need to set the fact we found the host key configurations. if conf == "HostKey" { foundHostKeys = true } } // Write this line to the new configuration. newConfig.WriteString(line) } // Go through each of the signed keys, and save them/add to the sshd configuration. for i, signedKey := range signResponse.SignedKeys { // The signed key result should be in the same order we sent them, // that means that the host key files will be in the same order. // The name of the host certificate file is the same as the public key, // but with `-cert` pre-pended to the extension. certKeyFile := strings.Replace(hostKeys[i], ".pub", "-cert.pub", 1) // Save the certificate file. f, err := os.Create(certKeyFile) if err != nil { return err } f.WriteString(signedKey) f.Close() os.Chmod(certKeyFile, 0644) // Append to the new sshd configuration file the certificate configuration. newConfig.WriteString("HostCertificate " + certKeyFile + "\n") } // Append the line we last read before we inserted our host certificates. newConfig.WriteString(lastReadLine) for { // Read the next line available. line, err := configReader.ReadString('\n') // If end of line, we are done reading. if err == io.EOF { break } // If error, something is wrong. if err != nil { return err } // Configurations in sshd_config is white space separated. If a whitepsace is not found, it is not a configuration line. i := strings.IndexByte(line, ' ') if i != -1 { // We pull the configuration name. conf := line[:i] // If this is a host certificate configuration, we need to ignore it. if conf == "HostCertificate" { continue } } // Write line to the new sshd configuration file. newConfig.WriteString(line) } // Check new configuration. newConfig.Close() fileinfo, err := os.Stat(sshdConfig + "_new") if err != nil { return err } // If new configuration is smaller than 256 bytes, something happened... if fileinfo.Size() <= 256 { os.Remove(sshdConfig + "_new") return fmt.Errorf("File size of new ssd_config is too small.") } // We can now replace the old configuration with new modified configuration. err = os.Rename(sshdConfig+"_new", sshdConfig) if err != nil { return err } // We need the PID of sshd so that we can signal it to re-load its configuration file. pidB, err := ioutil.ReadFile("/var/run/sshd.pid") if err != nil { return err } pid, err := strconv.Atoi(string(pidB[:len(pidB)-1])) if err != nil { return err } // Find the active process for SSHD based on its pid. sshd, err := os.FindProcess(pid) if err != nil { return err } // Signal SSHD to reload its configuration file. sshd.Signal(syscall.SIGHUP) } else { // The request was not successful, so there is likely a message with an error. fmt.Println(signResponse.Message) } return nil }