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  }