github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/tsa/tsacmd/command.go (about) 1 package tsacmd 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "net/http" 8 "os" 9 "sync" 10 "time" 11 12 "io/ioutil" 13 14 yaml "gopkg.in/yaml.v2" 15 16 "os/signal" 17 "syscall" 18 19 "code.cloudfoundry.org/lager" 20 "github.com/pf-qiu/concourse/v6/atc" 21 "github.com/pf-qiu/concourse/v6/tsa" 22 "github.com/concourse/flag" 23 "github.com/tedsuo/ifrit" 24 "github.com/tedsuo/ifrit/grouper" 25 "github.com/tedsuo/ifrit/http_server" 26 "github.com/tedsuo/ifrit/sigmon" 27 "golang.org/x/crypto/ssh" 28 "golang.org/x/oauth2" 29 "golang.org/x/oauth2/clientcredentials" 30 ) 31 32 type TSACommand struct { 33 Logger flag.Lager 34 35 BindIP flag.IP `long:"bind-ip" default:"0.0.0.0" description:"IP address on which to listen for SSH."` 36 PeerAddress string `long:"peer-address" default:"127.0.0.1" description:"Network address of this web node, reachable by other web nodes. Used for forwarded worker addresses."` 37 BindPort uint16 `long:"bind-port" default:"2222" description:"Port on which to listen for SSH."` 38 39 DebugBindIP flag.IP `long:"debug-bind-ip" default:"127.0.0.1" description:"IP address on which to listen for the pprof debugger endpoints."` 40 DebugBindPort uint16 `long:"debug-bind-port" default:"2221" description:"Port on which to listen for the pprof debugger endpoints."` 41 42 HostKey *flag.PrivateKey `long:"host-key" required:"true" description:"Path to private key to use for the SSH server."` 43 AuthorizedKeys flag.AuthorizedKeys `long:"authorized-keys" description:"Path to file containing keys to authorize, in SSH authorized_keys format (one public key per line)."` 44 TeamAuthorizedKeys map[string]flag.AuthorizedKeys `long:"team-authorized-keys" value-name:"NAME:PATH" description:"Path to file containing keys to authorize, in SSH authorized_keys format (one public key per line)."` 45 TeamAuthorizedKeysFile flag.File `long:"team-authorized-keys-file" description:"Path to file containing a YAML array of teams and their authorized SSH keys, e.g. [{team:foo,ssh_keys:[key1,key2]}]."` 46 47 ATCURLs []flag.URL `long:"atc-url" required:"true" description:"ATC API endpoints to which workers will be registered."` 48 49 ClientID string `long:"client-id" default:"concourse-worker" description:"Client used to fetch a token from the auth server. NOTE: if you change this value you will also need to change the --system-claim-value flag so the atc knows to allow requests from this client."` 50 ClientSecret string `long:"client-secret" required:"true" description:"Client used to fetch a token from the auth server"` 51 TokenURL flag.URL `long:"token-url" required:"true" description:"Token endpoint of the auth server"` 52 Scopes []string `long:"scope" description:"Scopes to request from the auth server"` 53 54 HeartbeatInterval time.Duration `long:"heartbeat-interval" default:"30s" description:"interval on which to heartbeat workers to the ATC"` 55 GardenRequestTimeout time.Duration `long:"garden-request-timeout" default:"5m" description:"How long to wait for requests to Garden to complete. 0 means no timeout."` 56 57 ClusterName string `long:"cluster-name" description:"A name for this Concourse cluster, to be displayed on the dashboard page."` 58 LogClusterName bool `long:"log-cluster-name" description:"Log cluster name."` 59 } 60 61 type TeamAuthKeys struct { 62 Team string 63 AuthKeys []ssh.PublicKey 64 } 65 66 type yamlTeamAuthorizedKey struct { 67 Team string `yaml:"team"` 68 Keys []string `yaml:"ssh_keys,flow"` 69 } 70 71 func (cmd *TSACommand) Execute(args []string) error { 72 runner, err := cmd.Runner(args) 73 if err != nil { 74 return err 75 } 76 77 tsaServerMember := grouper.Member{ 78 Name: "tsa-server", 79 Runner: sigmon.New(runner), 80 } 81 82 tsaDebugMember := grouper.Member{ 83 Name: "debug-server", 84 Runner: http_server.New( 85 cmd.debugBindAddr(), 86 http.DefaultServeMux, 87 )} 88 89 members := []grouper.Member{ 90 tsaDebugMember, 91 tsaServerMember, 92 } 93 94 group := grouper.NewParallel(os.Interrupt, members) 95 return <-ifrit.Invoke(group).Wait() 96 } 97 98 func (cmd *TSACommand) Runner(args []string) (ifrit.Runner, error) { 99 logger, _ := cmd.constructLogger() 100 101 atcEndpointPicker := tsa.NewRandomATCEndpointPicker(cmd.ATCURLs) 102 103 teamAuthorizedKeys, err := cmd.loadTeamAuthorizedKeys() 104 if err != nil { 105 return nil, fmt.Errorf("failed to load team authorized keys: %s", err) 106 } 107 108 if len(cmd.AuthorizedKeys.Keys)+len(cmd.TeamAuthorizedKeys) == 0 { 109 logger.Info("starting-tsa-without-authorized-keys") 110 } 111 112 sessionAuthTeam := &sessionTeam{ 113 sessionTeams: make(map[string]string), 114 lock: &sync.RWMutex{}, 115 } 116 117 config, err := cmd.configureSSHServer(sessionAuthTeam, cmd.AuthorizedKeys.Keys, teamAuthorizedKeys) 118 if err != nil { 119 return nil, fmt.Errorf("failed to configure SSH server: %s", err) 120 } 121 122 listenAddr := fmt.Sprintf("%s:%d", cmd.BindIP, cmd.BindPort) 123 124 authConfig := clientcredentials.Config{ 125 ClientID: cmd.ClientID, 126 ClientSecret: cmd.ClientSecret, 127 TokenURL: cmd.TokenURL.URL.String(), 128 Scopes: cmd.Scopes, 129 } 130 131 ctx := context.Background() 132 133 tokenSource := authConfig.TokenSource(ctx) 134 httpClient := oauth2.NewClient(ctx, tokenSource) 135 136 server := &server{ 137 logger: logger, 138 heartbeatInterval: cmd.HeartbeatInterval, 139 cprInterval: 1 * time.Second, 140 atcEndpointPicker: atcEndpointPicker, 141 forwardHost: cmd.PeerAddress, 142 config: config, 143 httpClient: httpClient, 144 sessionTeam: sessionAuthTeam, 145 gardenRequestTimeout: cmd.GardenRequestTimeout, 146 } 147 // Starts a goroutine whose purpose is to listen to the 148 // SIGHUP syscall and reload configuration upon receiving the signal. 149 // For now it only reloads the TSACommand.AuthorizedKeys but 150 // other configuration can potentially be added. 151 go func() { 152 reloadWorkerKeys := make(chan os.Signal, 1) 153 defer close(reloadWorkerKeys) 154 signal.Notify(reloadWorkerKeys, syscall.SIGHUP) 155 for { 156 157 // Block until a signal is received. 158 <-reloadWorkerKeys 159 160 logger.Info("reloading-config") 161 162 err := cmd.AuthorizedKeys.Reload() 163 if err != nil { 164 logger.Error("failed to reload authorized keys file : %s", err) 165 continue 166 } 167 168 teamAuthorizedKeys, err = cmd.loadTeamAuthorizedKeys() 169 if err != nil { 170 logger.Error("failed to load team authorized keys : %s", err) 171 continue 172 } 173 174 // Reconfigure the SSH server with the new keys 175 config, err := cmd.configureSSHServer(sessionAuthTeam, cmd.AuthorizedKeys.Keys, teamAuthorizedKeys) 176 if err != nil { 177 logger.Error("failed to configure SSH server: %s", err) 178 continue 179 } 180 181 server.config = config 182 } 183 }() 184 185 return serverRunner{logger, server, listenAddr}, nil 186 } 187 188 func (cmd *TSACommand) constructLogger() (lager.Logger, *lager.ReconfigurableSink) { 189 logger, reconfigurableSink := cmd.Logger.Logger("tsa") 190 if cmd.LogClusterName { 191 logger = logger.WithData(lager.Data{ 192 "cluster": cmd.ClusterName, 193 }) 194 } 195 196 return logger, reconfigurableSink 197 } 198 199 func (cmd *TSACommand) loadTeamAuthorizedKeys() ([]TeamAuthKeys, error) { 200 var teamKeys []TeamAuthKeys 201 202 for teamName, keys := range cmd.TeamAuthorizedKeys { 203 teamKeys = append(teamKeys, TeamAuthKeys{ 204 Team: teamName, 205 AuthKeys: keys.Keys, 206 }) 207 } 208 209 // load TeamAuthorizedKeysFile 210 if cmd.TeamAuthorizedKeysFile != "" { 211 logger, _ := cmd.constructLogger() 212 var rawTeamAuthorizedKeys []yamlTeamAuthorizedKey 213 214 authorizedKeysBytes, err := ioutil.ReadFile(cmd.TeamAuthorizedKeysFile.Path()) 215 if err != nil { 216 return nil, fmt.Errorf("failed to read yaml authorized keys file: %s", err) 217 } 218 err = yaml.Unmarshal([]byte(authorizedKeysBytes), &rawTeamAuthorizedKeys) 219 if err != nil { 220 return nil, fmt.Errorf("failed to parse yaml authorized keys file: %s", err) 221 } 222 223 for _, t := range rawTeamAuthorizedKeys { 224 var teamAuthorizedKeys []ssh.PublicKey 225 for _, k := range t.Keys { 226 key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k)) 227 if err != nil { 228 logger.Error("load-team-authorized-keys-parse", fmt.Errorf("Invalid format, ignoring (%s): %s", k, err.Error())) 229 continue 230 } 231 logger.Info("load-team-authorized-keys-loaded", lager.Data{"team": t.Team, "key": k}) 232 teamAuthorizedKeys = append(teamAuthorizedKeys, key) 233 } 234 teamKeys = append(teamKeys, TeamAuthKeys{Team: t.Team, AuthKeys: teamAuthorizedKeys}) 235 } 236 } 237 238 return teamKeys, nil 239 } 240 241 func (cmd *TSACommand) configureSSHServer(sessionAuthTeam *sessionTeam, authorizedKeys []ssh.PublicKey, teamAuthorizedKeys []TeamAuthKeys) (*ssh.ServerConfig, error) { 242 certChecker := &ssh.CertChecker{ 243 IsUserAuthority: func(key ssh.PublicKey) bool { 244 return false 245 }, 246 247 IsHostAuthority: func(key ssh.PublicKey, address string) bool { 248 return false 249 }, 250 251 UserKeyFallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { 252 for _, k := range authorizedKeys { 253 if bytes.Equal(k.Marshal(), key.Marshal()) { 254 return nil, nil 255 } 256 } 257 258 for _, teamKeys := range teamAuthorizedKeys { 259 for _, k := range teamKeys.AuthKeys { 260 if bytes.Equal(k.Marshal(), key.Marshal()) { 261 sessionAuthTeam.AuthorizeTeam(string(conn.SessionID()), teamKeys.Team) 262 return nil, nil 263 } 264 } 265 } 266 267 return nil, fmt.Errorf("unknown public key") 268 }, 269 } 270 271 config := &ssh.ServerConfig{ 272 Config: atc.DefaultSSHConfig(), 273 PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { 274 return certChecker.Authenticate(conn, key) 275 }, 276 } 277 278 signer, err := ssh.NewSignerFromKey(cmd.HostKey) 279 if err != nil { 280 return nil, fmt.Errorf("failed to create signer from host key: %s", err) 281 } 282 283 config.AddHostKey(signer) 284 285 return config, nil 286 } 287 288 func (cmd *TSACommand) debugBindAddr() string { 289 return fmt.Sprintf("%s:%d", cmd.DebugBindIP, cmd.DebugBindPort) 290 }