func (s *directSessionTokenService) AssumeRole(user *User, role string, enableLDAPRoles bool) (*sts.Credentials, error) { var arn string = s.buildARN(role) log.Debug("Checking ARN %s against user %s (with access %s)", arn, user.Username, user.ARNs) if enableLDAPRoles { found := false for _, a := range user.ARNs { a = s.buildARN(a) if arn == a { found = true break } } log.Debug("Found %s", found) if !found { return nil, errors.New(fmt.Sprintf("User %s is not authorized to assume role %s!", user.Username, arn)) } } options := &sts.AssumeRoleParams{ DurationSeconds: 3600, // the maximum allowed for AssumeRole RoleArn: arn, RoleSessionName: user.Username, } r, err := s.sts.AssumeRole(options) if err != nil { return nil, err } return &r.Credentials, nil }
/* SSHChallenge performs the challenge-response process to authenticate a connecting client to its SSH keys. */ func (sm *server) SSHChallenge(m protocol.MessageReadWriteCloser) (*User, error) { for { challenge := make([]byte, 64) for i := 0; i < len(challenge); i++ { challenge[i] = byte(rand.Int() % 256) } response := &protocol.Message{ ServerResponse: &protocol.ServerResponse{ Challenge: &protocol.SSHChallenge{ Challenge: challenge, }, }, } err := m.Write(response) if err != nil { return nil, err } challengeResponseMessage, err := m.Read() if err != nil { return nil, err } r := challengeResponseMessage.GetServerRequest() if r == nil { return nil, errors.New("not a server request") } cr := r.GetChallengeResponse() if cr == nil { return nil, errors.New("not a server request") } // Compose this into the proper format for Authenticate. sig := &ssh.Signature{ Format: cr.GetFormat(), Blob: cr.GetSignature(), } verifiedUser, err := sm.authenticator.Authenticate("derp", challenge, sig) if err != nil { return nil, err } if verifiedUser != nil { log.Debug("Verification completed for user %s!", verifiedUser.Username) return verifiedUser, nil } // continue around the loop, letting the client try another key verificationFailure := &protocol.Message{ ServerResponse: &protocol.ServerResponse{ VerificationFailure: &protocol.SSHVerificationFailure{}, }, } err = m.Write(verificationFailure) if err != nil { return nil, err } } }
/* Update() searches LDAP for the current user set that supports the necessary properties for Hologram. TODO: call this at some point during verification failure so that keys that have been recently added to LDAP work, instead of requiring a server restart. */ func (luc *ldapUserCache) Update() error { start := time.Now() filter := "(sshPublicKey=*)" searchRequest := ldap.NewSearchRequest( luc.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, filter, []string{"sshPublicKey", luc.userAttr}, nil, ) searchResult, err := luc.server.Search(searchRequest) if err != nil { return err } for _, entry := range searchResult.Entries { username := entry.GetAttributeValue(luc.userAttr) userKeys := []ssh.PublicKey{} for _, eachKey := range entry.GetAttributeValues("sshPublicKey") { sshKeyBytes, _ := base64.StdEncoding.DecodeString(eachKey) userSSHKey, err := ssh.ParsePublicKey(sshKeyBytes) if err != nil { userSSHKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(eachKey)) if err != nil { log.Warning("SSH key parsing for user %s failed (key was '%s')! This key will not be added into LDAP.", username, eachKey) continue } } userKeys = append(userKeys, userSSHKey) } luc.users[username] = &User{ SSHKeys: userKeys, Username: username, } log.Debug("Information on %s (re-)generated.", username) } log.Debug("LDAP information re-cached.") luc.stats.Timing(1.0, "ldapCacheUpdate", time.Since(start)) return nil }
/* PingHandler returns the correct response for a ping. */ func (sm *server) HandlePing(m protocol.MessageReadWriteCloser, p *protocol.Ping) { log.Debug("Handling a ping request.") sm.stats.Counter(1.0, "messages.ping", 1) pingType := protocol.Ping_RESPONSE pingMsg := &protocol.Message{ Ping: &protocol.Ping{ Type: &pingType, }, } m.Write(pingMsg) }
func (luc *ldapUserCache) Authenticate(username string, challenge []byte, sshSig *ssh.Signature) ( *User, error) { // Loop through all of the keys and attempt verification. retUser, _ := luc._verify(username, challenge, sshSig) if retUser == nil { log.Debug("Could not find %s in the LDAP cache; updating from the server.", username) luc.stats.Counter(1.0, "ldapCacheMiss", 1) // We should update LDAP cache again to retry keys. luc.Update() return luc._verify(username, challenge, sshSig) } return retUser, nil }
func request(req *protocol.AgentRequest) (*protocol.AgentResponse, error) { client, err := local.NewClient("/var/run/hologram.sock") if err != nil { return nil, err } // Try to get to the user's SSH agent, for best compatibility. // However, some agents are broken, so we should also try to // include the ssh key contents. sshAgentSock := os.Getenv("SSH_AUTH_SOCK") req.SshAgentSock = &sshAgentSock // Send along the raw bytes of the SSH key. // TODO(silversupreme): Add in logic for id_dsa, id_ecdsa, etc. if sshDir, homeErr := homedir.Expand("~/.ssh"); homeErr == nil { sshFilename := fmt.Sprintf("%s/id_rsa", sshDir) if sshKeyBytes, keyReadErr := ioutil.ReadFile(sshFilename); keyReadErr == nil { req.SshKeyFile = sshKeyBytes } else { log.Debug("Falling back on DSA key.") // Fallback on a user's DSA key if they have one. sshFilename := fmt.Sprintf("%s/id_dsa", sshDir) if sshKeyBytes, keyReadErr := ioutil.ReadFile(sshFilename); keyReadErr == nil { req.SshKeyFile = sshKeyBytes } } } msg := &protocol.Message{ AgentRequest: req, } err = client.Write(msg) if err != nil { return nil, err } response, err := client.Read() if response.GetAgentResponse() == nil { return nil, fmt.Errorf("Unexpected response type: %v", response) } return response.GetAgentResponse(), nil }
/* ConnectionHandler is the root of the state machine created for each socket that is opened. */ func (sm *server) HandleConnection(m protocol.MessageReadWriteCloser) { // Loop as long as we have this connection alive. log.Debug("Opening new connection handler.") for { recvMsg, err := m.Read() if err != nil { // EOFs are normal, so we don't want to report them as errors. if err.Error() != "EOF" { log.Errorf("Error reading data from stream: %s", err.Error()) } // Right now the behaviour of this is to terminate the connection // when we run into an error; should it perhaps send a NAK response // and keep the connection open for another retry? break } if pingMsg := recvMsg.GetPing(); pingMsg != nil { sm.HandlePing(m, pingMsg) } else if reqMsg := recvMsg.GetServerRequest(); reqMsg != nil { sm.HandleServerRequest(m, reqMsg) } } }
/* HandleServerRequest handles the flow for messages that this server accepts from clients. */ func (sm *server) HandleServerRequest(m protocol.MessageReadWriteCloser, r *protocol.ServerRequest) { if assumeRoleMsg := r.GetAssumeRole(); assumeRoleMsg != nil { log.Debug("Handling an assumeRole request.") sm.stats.Counter(1.0, "messages.assumeRole", 1) role := assumeRoleMsg.GetRole() user, err := sm.SSHChallenge(m) if err != nil { log.Errorf("Error trying to handle AssumeRole: %s", err.Error()) m.Close() return } if user != nil { creds, err := sm.credentials.AssumeRole(user, role, sm.enableLDAPRoles) if err != nil { // error message from Amazon, so forward that on to the client errStr := err.Error() errMsg := &protocol.Message{ Error: &errStr, } log.Errorf("Error from AWS for AssumeRole: %s", err.Error()) m.Write(errMsg) sm.stats.Counter(1.0, "errors.assumeRole", 1) //m.Close() return } m.Write(makeCredsResponse(creds)) return } } else if getUserCredentialsMsg := r.GetGetUserCredentials(); getUserCredentialsMsg != nil { sm.stats.Counter(1.0, "messages.getUserCredentialsMsg", 1) user, err := sm.SSHChallenge(m) if err != nil { log.Errorf("Error trying to handle GetUserCredentials: %s", err.Error()) m.Close() return } if user != nil { creds, err := sm.credentials.AssumeRole(user, sm.DefaultRole, sm.enableLDAPRoles) if err != nil { log.Errorf("Error trying to handle GetUserCredentials: %s", err.Error()) m.Close() return } m.Write(makeCredsResponse(creds)) return } } else if addSSHKeyMsg := r.GetAddSSHkey(); addSSHKeyMsg != nil { sm.stats.Counter(1.0, "messages.addSSHKeyMsg", 1) // Search for the user specified in this request. sr := ldap.NewSearchRequest( sm.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, fmt.Sprintf("(%s=%s)", sm.userAttr, addSSHKeyMsg.GetUsername()), []string{"sshPublicKey", sm.userAttr, "userPassword"}, nil) user, err := sm.ldapServer.Search(sr) if err != nil { log.Errorf("Error trying to handle addSSHKeyMsg: %s", err.Error()) return } if len(user.Entries) == 0 { log.Errorf("User %s not found!", addSSHKeyMsg.GetUsername()) return } // Check their password. password := user.Entries[0].GetAttributeValue("userPassword") if password != addSSHKeyMsg.GetPasswordhash() { log.Errorf("Provided password for user %s does not match %s!", addSSHKeyMsg.GetUsername(), password) return } // Check to see if this SSH key already exists. for _, k := range user.Entries[0].GetAttributeValues("sshPublicKey") { if k == addSSHKeyMsg.GetSshkeybytes() { log.Warning("User %s already has this SSH key. Doing nothing.", addSSHKeyMsg.GetUsername()) successMsg := &protocol.Message{Success: &protocol.Success{}} m.Write(successMsg) return } } mr := ldap.NewModifyRequest(user.Entries[0].DN) mr.Add("sshPublicKey", []string{addSSHKeyMsg.GetSshkeybytes()}) err = sm.ldapServer.Modify(mr) if err != nil { log.Errorf("Could not modify LDAP user: %s", err.Error()) return } successMsg := &protocol.Message{Success: &protocol.Success{}} m.Write(successMsg) return } }
func main() { // Parse command-line flags for this system. var ( listenAddress = flag.String("addr", "", "Address to listen to incoming requests on.") ldapAddress = flag.String("ldapAddr", "", "Address to connect to LDAP.") ldapBindDN = flag.String("ldapBindDN", "", "LDAP DN to bind to for login.") ldapInsecure = flag.Bool("insecureLDAP", false, "INSECURE: Don't use TLS for LDAP connection.") ldapBindPassword = flag.String("ldapBindPassword", "", "LDAP password for bind.") statsdHost = flag.String("statsHost", "", "Address to send statsd metrics to.") iamAccount = flag.String("iamaccount", "", "AWS Account ID for generating IAM Role ARNs") enableLDAPRoles = flag.Bool("ldaproles", false, "Enable role support using LDAP directory.") roleAttribute = flag.String("roleattribute", "", "Group attribute to get role from.") defaultRole = flag.String("role", "", "AWS role to assume by default.") configFile = flag.String("conf", "/etc/hologram/server.json", "Config file to load.") cacheTimeout = flag.Int("cachetime", 3600, "Time in seconds after which to refresh LDAP user cache.") debugMode = flag.Bool("debug", false, "Enable debug mode.") config Config ) flag.Parse() // Enable debug log output if the user requested it. if *debugMode { log.DebugMode(true) log.Debug("Enabling debug log output. Use sparingly.") } // Parse in options from the given config file. log.Debug("Loading configuration from %s", *configFile) configContents, configErr := ioutil.ReadFile(*configFile) if configErr != nil { log.Errorf("Could not read from config file. The error was: %s", configErr.Error()) os.Exit(1) } configParseErr := json.Unmarshal(configContents, &config) if configParseErr != nil { log.Errorf("Error in parsing config file: %s", configParseErr.Error()) os.Exit(1) } // Merge in command flag options. if *ldapAddress != "" { config.LDAP.Host = *ldapAddress } if *ldapInsecure { config.LDAP.InsecureLDAP = true } if *ldapBindDN != "" { config.LDAP.Bind.DN = *ldapBindDN } if *ldapBindPassword != "" { config.LDAP.Bind.Password = *ldapBindPassword } if *statsdHost != "" { config.Stats = *statsdHost } if *iamAccount != "" { config.AWS.Account = *iamAccount } if *listenAddress != "" { config.Listen = *listenAddress } if *defaultRole != "" { config.AWS.DefaultRole = *defaultRole } if *enableLDAPRoles { config.LDAP.EnableLDAPRoles = true } if *roleAttribute != "" { config.LDAP.RoleAttribute = *roleAttribute } if *cacheTimeout != 3600 { config.CacheTimeout = *cacheTimeout } var stats g2s.Statter var statsErr error if config.LDAP.UserAttr == "" { config.LDAP.UserAttr = "cn" } if config.Stats == "" { log.Debug("No statsd server specified; no metrics will be emitted by this program.") stats = g2s.Noop() } else { stats, statsErr = g2s.Dial("udp", config.Stats) if statsErr != nil { log.Errorf("Error connecting to statsd: %s. No metrics will be emitted by this program.", statsErr.Error()) stats = g2s.Noop() } else { log.Debug("This program will emit metrics to %s", config.Stats) } } // Setup the server state machine that responds to requests. auth, err := aws.GetAuth(os.Getenv("HOLOGRAM_AWSKEY"), os.Getenv("HOLOGRAM_AWSSECRET"), "", time.Now()) if err != nil { log.Errorf("Error getting instance credentials: %s", err.Error()) os.Exit(1) } stsConnection := sts.New(auth, aws.Regions["us-east-1"]) credentialsService := server.NewDirectSessionTokenService(config.AWS.Account, stsConnection) var ldapServer *ldap.Conn // Connect to the LDAP server using TLS or not depending on the config if config.LDAP.InsecureLDAP { log.Debug("Connecting to LDAP at server %s (NOT using TLS).", config.LDAP.Host) ldapServer, err = ldap.Dial("tcp", config.LDAP.Host) if err != nil { log.Errorf("Could not dial LDAP! %s", err.Error()) os.Exit(1) } } else { // Connect to the LDAP server with sample credentials. tlsConfig := &tls.Config{ InsecureSkipVerify: true, } log.Debug("Connecting to LDAP at server %s.", config.LDAP.Host) ldapServer, err = ldap.DialTLS("tcp", config.LDAP.Host, tlsConfig) if err != nil { log.Errorf("Could not dial LDAP! %s", err.Error()) os.Exit(1) } } if bindErr := ldapServer.Bind(config.LDAP.Bind.DN, config.LDAP.Bind.Password); bindErr != nil { log.Errorf("Could not bind to LDAP! %s", bindErr.Error()) os.Exit(1) } ldapCache, err := server.NewLDAPUserCache(ldapServer, stats, config.LDAP.UserAttr, config.LDAP.BaseDN, config.LDAP.EnableLDAPRoles, config.LDAP.RoleAttribute) if err != nil { log.Errorf("Top-level error in LDAPUserCache layer: %s", err.Error()) os.Exit(1) } serverHandler := server.New(ldapCache, credentialsService, config.AWS.DefaultRole, stats, ldapServer, config.LDAP.UserAttr, config.LDAP.BaseDN, config.LDAP.EnableLDAPRoles) server, err := remote.NewServer(config.Listen, serverHandler.HandleConnection) // Wait for a signal from the OS to shutdown. terminate := make(chan os.Signal) signal.Notify(terminate, syscall.SIGINT, syscall.SIGTERM) // SIGUSR1 and SIGUSR2 should make Hologram enable and disable debug logging, // respectively. debugEnable := make(chan os.Signal) debugDisable := make(chan os.Signal) signal.Notify(debugEnable, syscall.SIGUSR1) signal.Notify(debugDisable, syscall.SIGUSR2) // SIGHUP should make Hologram server reload its cache of user information // from LDAP. reloadCacheSigHup := make(chan os.Signal) signal.Notify(reloadCacheSigHup, syscall.SIGHUP) // Reload the cache based on time set in configuration cacheTimeoutTicker := time.NewTicker(time.Duration(config.CacheTimeout) * time.Second) log.Info("Hologram server is online, waiting for termination.") WaitForTermination: for { select { case <-terminate: break WaitForTermination case <-debugEnable: log.Info("Enabling debug mode.") log.DebugMode(true) case <-debugDisable: log.Info("Disabling debug mode.") log.DebugMode(false) case <-reloadCacheSigHup: log.Info("Force-reloading user cache.") ldapCache.Update() case <-cacheTimeoutTicker.C: log.Info("Cache timeout. Reloading user cache.") ldapCache.Update() } } log.Info("Caught signal; shutting down now.") server.Close() }
func main() { flag.Parse() if *debugMode { log.DebugMode(true) log.Debug("Enabling debug mode. Use sparingly.") } // Parse in options from the given config file. log.Debug("Loading configuration from %s", *configFile) configContents, configErr := ioutil.ReadFile(*configFile) if configErr != nil { log.Errorf("Could not read from config file. The error was: %s", configErr.Error()) os.Exit(1) } configParseErr := json.Unmarshal(configContents, &config) if configParseErr != nil { log.Errorf("Error in parsing config file: %s", configParseErr.Error()) os.Exit(1) } // Resolve configuration from the file and commond-line flags. // Flags will always take precedence. if *dialAddress != "" { log.Debug("Using command-line remote address.") config.Host = *dialAddress } // Emit the final config options for debugging if requested. log.Debug("Final config:") log.Debug("Hologram server address: %s", config.Host) defer func() { log.Debug("Removing UNIX socket.") os.Remove("/var/run/hologram.sock") }() // Startup the HTTP server and respond to requests. listener, err := net.ListenTCP("tcp", &net.TCPAddr{ IP: net.ParseIP("169.254.169.254"), Port: 80, }) if err != nil { log.Errorf("Could not startup the metadata interface: %s", err) os.Exit(1) } credsManager := agent.NewCredentialsExpirationManager() mds, metadataError := agent.NewMetadataService(listener, credsManager) if metadataError != nil { log.Errorf("Could not create metadata service: %s", metadataError.Error()) os.Exit(1) } mds.Start() // Create a hologram client that can be used by other services to talk to the server client := agent.NewClient(config.Host, credsManager) agentServer := agent.NewCliHandler("/var/run/hologram.sock", client) err = agentServer.Start() if err != nil { log.Errorf("Could not start agentServer: %s", err.Error()) os.Exit(1) } // Wait for a graceful shutdown signal terminate := make(chan os.Signal) signal.Notify(terminate, syscall.SIGINT, syscall.SIGTERM) // SIGUSR1 and SIGUSR2 should make Hologram enable and disable debug logging, // respectively. debugEnable := make(chan os.Signal) debugDisable := make(chan os.Signal) signal.Notify(debugEnable, syscall.SIGUSR1) signal.Notify(debugDisable, syscall.SIGUSR2) log.Info("Hologram agent is online, waiting for termination.") WaitForTermination: for { select { case <-terminate: break WaitForTermination case <-debugEnable: log.Info("Enabling debug mode.") log.DebugMode(true) case <-debugDisable: log.Info("Disabling debug mode.") log.DebugMode(false) } } log.Info("Caught signal; shutting down now.") }
func (h *cliHandler) HandleConnection(c protocol.MessageReadWriteCloser) { for { msg, err := c.Read() if err != nil { return } if msg.GetAgentRequest() != nil { dr := msg.GetAgentRequest() var ( sshAgentSock string sshKeyBytes []byte ) sshAgentSock = dr.GetSshAgentSock() if sshAgentSock != "" { log.Debug("SSH_AUTH_SOCK included in this request: %s", sshAgentSock) } sshKeyBytes = dr.GetSshKeyFile() if sshKeyBytes != nil { log.Debug("SSH keyfile included in this request.") } SSHSetAgentSock(sshAgentSock, sshKeyBytes) if dr.GetAssumeRole() != nil { log.Debug("Handling AssumeRole request.") assumeRole := dr.GetAssumeRole() err := h.client.AssumeRole(assumeRole.GetRole()) var agentResponse protocol.AgentResponse if err == nil { agentResponse.Success = &protocol.Success{} } else { log.Errorf(err.Error()) e := err.Error() agentResponse.Failure = &protocol.Failure{ ErrorMessage: &e, } } msg = &protocol.Message{ AgentResponse: &agentResponse, } err = c.Write(msg) if err != nil { return } } else if dr.GetGetUserCredentials() != nil { log.Debug("Handling GetSessionToken request.") err := h.client.GetUserCredentials() var agentResponse protocol.AgentResponse if err == nil { agentResponse.Success = &protocol.Success{} } else { log.Errorf(err.Error()) e := err.Error() agentResponse.Failure = &protocol.Failure{ ErrorMessage: &e, } } msg = &protocol.Message{ AgentResponse: &agentResponse, } err = c.Write(msg) if err != nil { return } } else { log.Errorf("Unexpected agent request: %s", dr) c.Close() return } } else { log.Errorf("Unexpected message: %s", msg) c.Close() return } } }
/* Update() searches LDAP for the current user set that supports the necessary properties for Hologram. TODO: call this at some point during verification failure so that keys that have been recently added to LDAP work, instead of requiring a server restart. */ func (luc *ldapUserCache) Update() error { start := time.Now() if luc.enableLDAPRoles { groupSearchRequest := ldap.NewSearchRequest( luc.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=groupOfNames)", []string{luc.roleAttribute}, nil, ) groupSearchResult, err := luc.server.Search(groupSearchRequest) if err != nil { return err } for _, entry := range groupSearchResult.Entries { dn := entry.DN arns := entry.GetAttributeValues(luc.roleAttribute) log.Debug("Adding %s to %s", arns, dn) luc.groups[dn] = arns } } filter := "(sshPublicKey=*)" searchRequest := ldap.NewSearchRequest( luc.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, filter, []string{"sshPublicKey", luc.userAttr, "memberOf"}, nil, ) searchResult, err := luc.server.Search(searchRequest) if err != nil { return err } for _, entry := range searchResult.Entries { username := entry.GetAttributeValue(luc.userAttr) userKeys := []ssh.PublicKey{} for _, eachKey := range entry.GetAttributeValues("sshPublicKey") { sshKeyBytes, _ := base64.StdEncoding.DecodeString(eachKey) userSSHKey, err := ssh.ParsePublicKey(sshKeyBytes) if err != nil { userSSHKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(eachKey)) if err != nil { log.Warning("SSH key parsing for user %s failed (key was '%s')! This key will not be added into LDAP.", username, eachKey) continue } } userKeys = append(userKeys, userSSHKey) } arns := []string{} if luc.enableLDAPRoles { for _, groupDN := range entry.GetAttributeValues("memberOf") { log.Debug(groupDN) arns = append(arns, luc.groups[groupDN]...) } } luc.users[username] = &User{ SSHKeys: userKeys, Username: username, ARNs: arns, } log.Debug("Information on %s (re-)generated.", username) } log.Debug("LDAP information re-cached.") luc.stats.Timing(1.0, "ldapCacheUpdate", time.Since(start)) return nil }