mirror of https://github.com/k3s-io/k3s
Simplify token parsing
Improves readability, reduces round-trips to the join server to validate certs. Signed-off-by: Brad Davidson <brad.davidson@rancher.com>pull/2300/head
parent
9074da7405
commit
45dd4afe50
|
@ -54,13 +54,12 @@ func Get(ctx context.Context, agent cmds.Agent, proxy proxy.Proxy) *config.Node
|
||||||
type HTTPRequester func(u string, client *http.Client, username, password string) ([]byte, error)
|
type HTTPRequester func(u string, client *http.Client, username, password string) ([]byte, error)
|
||||||
|
|
||||||
func Request(path string, info *clientaccess.Info, requester HTTPRequester) ([]byte, error) {
|
func Request(path string, info *clientaccess.Info, requester HTTPRequester) ([]byte, error) {
|
||||||
u, err := url.Parse(info.URL)
|
u, err := url.Parse(info.BaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u.Path = path
|
u.Path = path
|
||||||
username, password, _ := clientaccess.ParseUsernamePassword(info.Token)
|
return requester(u.String(), clientaccess.GetHTTPClient(info.CACerts), info.Username, info.Password)
|
||||||
return requester(u.String(), clientaccess.GetHTTPClient(info.CACerts), username, password)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNodeNamedCrt(nodeName, nodeIP, nodePasswordFile string) HTTPRequester {
|
func getNodeNamedCrt(nodeName, nodeIP, nodePasswordFile string) HTTPRequester {
|
||||||
|
|
|
@ -152,7 +152,7 @@ func Run(ctx context.Context, cfg cmds.Agent) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
newToken, err := clientaccess.NormalizeAndValidateTokenForUser(proxy.SupervisorURL(), cfg.Token, "node")
|
newToken, err := clientaccess.ParseAndValidateTokenForUser(proxy.SupervisorURL(), cfg.Token, "node")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Error(err)
|
logrus.Error(err)
|
||||||
select {
|
select {
|
||||||
|
@ -162,7 +162,7 @@ func Run(ctx context.Context, cfg cmds.Agent) error {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cfg.Token = newToken
|
cfg.Token = newToken.String()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,15 +26,15 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenPrefix = "K10"
|
||||||
|
tokenFormat = "%s%s::%s:%s"
|
||||||
|
)
|
||||||
|
|
||||||
type OverrideURLCallback func(config []byte) (*url.URL, error)
|
type OverrideURLCallback func(config []byte) (*url.URL, error)
|
||||||
|
|
||||||
type clientToken struct {
|
// WriteClientKubeConfig generates a kubeconfig at destFile that can be used to connect to a server at url with the given certs and keys
|
||||||
caHash string
|
func WriteClientKubeConfig(destFile string, url string, serverCAFile string, clientCertFile string, clientKeyFile string) error {
|
||||||
username string
|
|
||||||
password string
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteClientKubeConfig(destFile, url, serverCAFile, clientCertFile, clientKeyFile string) error {
|
|
||||||
serverCA, err := ioutil.ReadFile(serverCAFile)
|
serverCA, err := ioutil.ReadFile(serverCAFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to read %s", serverCAFile)
|
return errors.Wrapf(err, "failed to read %s", serverCAFile)
|
||||||
|
@ -73,96 +73,71 @@ func WriteClientKubeConfig(destFile, url, serverCAFile, clientCertFile, clientKe
|
||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
CACerts []byte `json:"cacerts,omitempty"`
|
CACerts []byte `json:"cacerts,omitempty"`
|
||||||
username string
|
BaseURL string `json:"baseurl,omitempty"`
|
||||||
password string
|
Username string `json:"username,omitempty"`
|
||||||
Token string `json:"token,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
caHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Info) ToToken() string {
|
// String returns the token data, templated according to the token format
|
||||||
return fmt.Sprintf("K10%s::%s:%s", hashCA(i.CACerts), i.username, i.password)
|
func (info *Info) String() string {
|
||||||
|
return fmt.Sprintf(tokenFormat, tokenPrefix, hashCA(info.CACerts), info.Username, info.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeAndValidateTokenForUser(server, token, user string) (string, error) {
|
// ParseAndValidateToken parses a token, downloads and validates the server's CA bundle,
|
||||||
if !strings.HasPrefix(token, "K10") {
|
// and validates it according to the caHash from the token if set.
|
||||||
token = "K10::" + user + ":" + token
|
func ParseAndValidateToken(server string, token string) (*Info, error) {
|
||||||
}
|
info, err := parseToken(token)
|
||||||
info, err := ParseAndValidateToken(server, token)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.username != user {
|
|
||||||
info.username = user
|
|
||||||
}
|
|
||||||
|
|
||||||
return info.ToToken(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAndValidateToken(server, token string) (*Info, error) {
|
|
||||||
url, err := url.Parse(server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "Invalid url, failed to parse %s", server)
|
|
||||||
}
|
|
||||||
|
|
||||||
if url.Scheme != "https" {
|
|
||||||
return nil, fmt.Errorf("only https:// URLs are supported, invalid scheme: %s", server)
|
|
||||||
}
|
|
||||||
|
|
||||||
for strings.HasSuffix(url.Path, "/") {
|
|
||||||
url.Path = url.Path[:len(url.Path)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedToken, err := parseToken(token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cacerts, err := GetCACerts(*url)
|
if err := info.setServer(server); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cacerts) > 0 && len(parsedToken.caHash) > 0 {
|
if info.caHash != "" {
|
||||||
if ok, hash, newHash := validateCACerts(cacerts, parsedToken.caHash); !ok {
|
if err := info.validateCAHash(); err != nil {
|
||||||
return nil, fmt.Errorf("token does not match the server %s != %s", hash, newHash)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateToken(*url, cacerts, parsedToken.username, parsedToken.password); err != nil {
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAndValidateToken parses a token with user override, downloads and
|
||||||
|
// validates the server's CA bundle, and validates it according to the caHash from the token if set.
|
||||||
|
func ParseAndValidateTokenForUser(server string, token string, username string) (*Info, error) {
|
||||||
|
info, err := parseToken(token)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
i := &Info{
|
info.Username = username
|
||||||
URL: url.String(),
|
|
||||||
CACerts: cacerts,
|
if err := info.setServer(server); err != nil {
|
||||||
username: parsedToken.username,
|
return nil, err
|
||||||
password: parsedToken.password,
|
|
||||||
Token: token,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalize token
|
if info.caHash != "" {
|
||||||
i.Token = i.ToToken()
|
if err := info.validateCAHash(); err != nil {
|
||||||
return i, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateToken(u url.URL, cacerts []byte, username, password string) error {
|
|
||||||
u.Path = "/cacerts"
|
|
||||||
_, err := get(u.String(), GetHTTPClient(cacerts), username, password)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "token is not valid")
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCACerts(cacerts []byte, hash string) (bool, string, string) {
|
// validateCACerts returns a boolean indicating whether or not a CA bundle matches the provided hash,
|
||||||
|
// and a string containing the hash of the CA bundle.
|
||||||
|
func validateCACerts(cacerts []byte, hash string) (bool, string) {
|
||||||
if len(cacerts) == 0 && hash == "" {
|
if len(cacerts) == 0 && hash == "" {
|
||||||
return true, "", ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
newHash := hashCA(cacerts)
|
newHash := hashCA(cacerts)
|
||||||
return hash == newHash, hash, newHash
|
return hash == newHash, newHash
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashCA returns the hex-encoded SHA256 digest of a byte array.
|
// hashCA returns the hex-encoded SHA256 digest of a byte array.
|
||||||
|
@ -174,38 +149,40 @@ func hashCA(cacerts []byte) string {
|
||||||
// ParseUsernamePassword returns the username and password portion of a token string,
|
// ParseUsernamePassword returns the username and password portion of a token string,
|
||||||
// along with a bool indicating if the token was successfully parsed.
|
// along with a bool indicating if the token was successfully parsed.
|
||||||
func ParseUsernamePassword(token string) (string, string, bool) {
|
func ParseUsernamePassword(token string) (string, string, bool) {
|
||||||
parsed, err := parseToken(token)
|
info, err := parseToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
return parsed.username, parsed.password, true
|
return info.Username, info.Password, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseToken(token string) (clientToken, error) {
|
// parseToken parses a token into an Info struct
|
||||||
var result clientToken
|
func parseToken(token string) (*Info, error) {
|
||||||
|
var info = &Info{}
|
||||||
|
|
||||||
if !strings.HasPrefix(token, "K10") {
|
if !strings.HasPrefix(token, tokenPrefix) {
|
||||||
return result, fmt.Errorf("token is not a valid token format")
|
token = fmt.Sprintf(tokenFormat, tokenPrefix, "", "", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
token = token[3:]
|
// Strip off the prefix
|
||||||
|
token = token[len(tokenPrefix):]
|
||||||
|
|
||||||
parts := strings.SplitN(token, "::", 2)
|
parts := strings.SplitN(token, "::", 2)
|
||||||
token = parts[0]
|
token = parts[0]
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
result.caHash = parts[0]
|
info.caHash = parts[0]
|
||||||
token = parts[1]
|
token = parts[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
parts = strings.SplitN(token, ":", 2)
|
parts = strings.SplitN(token, ":", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return result, fmt.Errorf("token credentials are the wrong format")
|
return nil, fmt.Errorf("invalid token format")
|
||||||
}
|
}
|
||||||
|
|
||||||
result.username = parts[0]
|
info.Username = parts[0]
|
||||||
result.password = parts[1]
|
info.Password = parts[1]
|
||||||
|
|
||||||
return result, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHTTPClient returns a http client that validates TLS server certificates using the provided CA bundle.
|
// GetHTTPClient returns a http client that validates TLS server certificates using the provided CA bundle.
|
||||||
|
@ -232,18 +209,53 @@ func GetHTTPClient(cacerts []byte) *http.Client {
|
||||||
|
|
||||||
// Get makes a request to a subpath of info's BaseURL
|
// Get makes a request to a subpath of info's BaseURL
|
||||||
func Get(path string, info *Info) ([]byte, error) {
|
func Get(path string, info *Info) ([]byte, error) {
|
||||||
u, err := url.Parse(info.URL)
|
u, err := url.Parse(info.BaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u.Path = path
|
u.Path = path
|
||||||
return get(u.String(), GetHTTPClient(info.CACerts), info.username, info.password)
|
return get(u.String(), GetHTTPClient(info.CACerts), info.Username, info.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setServer sets the BaseURL and CACerts fields of the Info by connecting to the server
|
||||||
|
// and storing the CA bundle.
|
||||||
|
func (info *Info) setServer(server string) error {
|
||||||
|
url, err := url.Parse(server)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Invalid server url, failed to parse: %s", server)
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.Scheme != "https" {
|
||||||
|
return fmt.Errorf("only https:// URLs are supported, invalid scheme: %s", server)
|
||||||
|
}
|
||||||
|
|
||||||
|
for strings.HasSuffix(url.Path, "/") {
|
||||||
|
url.Path = url.Path[:len(url.Path)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
cacerts, err := getCACerts(*url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info.BaseURL = url.String()
|
||||||
|
info.CACerts = cacerts
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCAHash validates that info's caHash matches the CACerts hash.
|
||||||
|
func (info *Info) validateCAHash() error {
|
||||||
|
if ok, serverHash := validateCACerts(info.CACerts, info.caHash); !ok {
|
||||||
|
return fmt.Errorf("token CA hash does not match the server CA hash: %s != %s", info.caHash, serverHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCACerts retrieves the CA bundle from a server.
|
// getCACerts retrieves the CA bundle from a server.
|
||||||
// An error is raised if the CA bundle cannot be retrieved,
|
// An error is raised if the CA bundle cannot be retrieved,
|
||||||
// or if the server's cert is not signed by the returned bundle.
|
// or if the server's cert is not signed by the returned bundle.
|
||||||
func GetCACerts(u url.URL) ([]byte, error) {
|
func getCACerts(u url.URL) ([]byte, error) {
|
||||||
u.Path = "/cacerts"
|
u.Path = "/cacerts"
|
||||||
url := u.String()
|
url := u.String()
|
||||||
|
|
||||||
|
@ -258,7 +270,7 @@ func GetCACerts(u url.URL) ([]byte, error) {
|
||||||
// Download the CA bundle using a client that does not validate certs.
|
// Download the CA bundle using a client that does not validate certs.
|
||||||
cacerts, err := get(url, insecureClient, "", "")
|
cacerts, err := get(url, insecureClient, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to get CA certs at %s", url)
|
return nil, errors.Wrap(err, "failed to get CA certs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request the CA bundle again, validating that the CA bundle can be loaded
|
// Request the CA bundle again, validating that the CA bundle can be loaded
|
||||||
|
@ -266,7 +278,7 @@ func GetCACerts(u url.URL) ([]byte, error) {
|
||||||
// get an empty CA bundle. or if the dynamiclistener cert is incorrectly signed.
|
// get an empty CA bundle. or if the dynamiclistener cert is incorrectly signed.
|
||||||
_, err = get(url, GetHTTPClient(cacerts), "", "")
|
_, err = get(url, GetHTTPClient(cacerts), "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "server %s is not trusted", url)
|
return nil, errors.Wrap(err, "CA cert validation failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacerts, nil
|
return cacerts, nil
|
||||||
|
|
|
@ -64,12 +64,9 @@ func (c *Cluster) shouldBootstrapLoad(ctx context.Context) (bool, error) {
|
||||||
return false, errors.New(version.ProgramUpper + "_TOKEN is required to join a cluster")
|
return false, errors.New(version.ProgramUpper + "_TOKEN is required to join a cluster")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := clientaccess.NormalizeAndValidateTokenForUser(c.config.JoinURL, c.config.Token, "server")
|
// Fail if the token isn't syntactically valid, or if the CA hash on the remote server doesn't match
|
||||||
if err != nil {
|
// the hash in the token. The password isn't actually checked until later when actually bootstrapping.
|
||||||
return false, err
|
info, err := clientaccess.ParseAndValidateTokenForUser(c.config.JoinURL, c.config.Token, "server")
|
||||||
}
|
|
||||||
|
|
||||||
info, err := clientaccess.ParseAndValidateToken(c.config.JoinURL, token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue