mirror of
https://github.com/k3s-io/k3s.git
synced 2024-06-07 19:41:36 +00:00
2f5ee914f9
In k3s today the kubernetes API and the /v1-k3s API are combined into one http server. In rke2 we are running unmodified, non-embedded Kubernetes and as such it is preferred to run k8s and the /v1-k3s API on different ports. The /v1-k3s API port is called the SupervisorPort in the code. To support this separation of ports a new shim was added on the client in then pkg/agent/proxy package that will launch two load balancers instead of just one load balancer. One load balancer for 6443 and the other for 9345 (which is the supervisor port).
279 lines
6.0 KiB
Go
279 lines
6.0 KiB
Go
package clientaccess
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
)
|
|
|
|
var (
|
|
insecureClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
type OverrideURLCallback func(config []byte) (*url.URL, error)
|
|
|
|
type clientToken struct {
|
|
caHash string
|
|
username string
|
|
password string
|
|
}
|
|
|
|
func WriteClientKubeConfig(destFile, url, serverCAFile, clientCertFile, clientKeyFile string) error {
|
|
serverCA, err := ioutil.ReadFile(serverCAFile)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to read %s", serverCAFile)
|
|
}
|
|
|
|
clientCert, err := ioutil.ReadFile(clientCertFile)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to read %s", clientCertFile)
|
|
}
|
|
|
|
clientKey, err := ioutil.ReadFile(clientKeyFile)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to read %s", clientKeyFile)
|
|
}
|
|
|
|
config := clientcmdapi.NewConfig()
|
|
|
|
cluster := clientcmdapi.NewCluster()
|
|
cluster.CertificateAuthorityData = serverCA
|
|
cluster.Server = url
|
|
|
|
authInfo := clientcmdapi.NewAuthInfo()
|
|
authInfo.ClientCertificateData = clientCert
|
|
authInfo.ClientKeyData = clientKey
|
|
|
|
context := clientcmdapi.NewContext()
|
|
context.AuthInfo = "default"
|
|
context.Cluster = "default"
|
|
|
|
config.Clusters["default"] = cluster
|
|
config.AuthInfos["default"] = authInfo
|
|
config.Contexts["default"] = context
|
|
config.CurrentContext = "default"
|
|
|
|
return clientcmd.WriteToFile(*config, destFile)
|
|
}
|
|
|
|
type Info struct {
|
|
URL string `json:"url,omitempty"`
|
|
CACerts []byte `json:"cacerts,omitempty"`
|
|
username string
|
|
password string
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
func (i *Info) ToToken() string {
|
|
return fmt.Sprintf("K10%s::%s:%s", hashCA(i.CACerts), i.username, i.password)
|
|
}
|
|
|
|
func NormalizeAndValidateTokenForUser(server, token, user string) (string, error) {
|
|
if !strings.HasPrefix(token, "K10") {
|
|
token = "K10::" + user + ":" + 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 {
|
|
return nil, err
|
|
}
|
|
|
|
cacerts, err := GetCACerts(*url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(cacerts) > 0 && len(parsedToken.caHash) > 0 {
|
|
if ok, hash, newHash := validateCACerts(cacerts, parsedToken.caHash); !ok {
|
|
return nil, fmt.Errorf("token does not match the server %s != %s", hash, newHash)
|
|
}
|
|
}
|
|
|
|
if err := validateToken(*url, cacerts, parsedToken.username, parsedToken.password); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
i := &Info{
|
|
URL: url.String(),
|
|
CACerts: cacerts,
|
|
username: parsedToken.username,
|
|
password: parsedToken.password,
|
|
Token: token,
|
|
}
|
|
|
|
// normalize token
|
|
i.Token = i.ToToken()
|
|
return i, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func validateCACerts(cacerts []byte, hash string) (bool, string, string) {
|
|
if len(cacerts) == 0 && hash == "" {
|
|
return true, "", ""
|
|
}
|
|
|
|
newHash := hashCA(cacerts)
|
|
return hash == newHash, hash, newHash
|
|
}
|
|
|
|
func hashCA(cacerts []byte) string {
|
|
digest := sha256.Sum256(cacerts)
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func ParseUsernamePassword(token string) (string, string, bool) {
|
|
parsed, err := parseToken(token)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
return parsed.username, parsed.password, true
|
|
}
|
|
|
|
func parseToken(token string) (clientToken, error) {
|
|
var result clientToken
|
|
|
|
if !strings.HasPrefix(token, "K10") {
|
|
return result, fmt.Errorf("token is not a valid token format")
|
|
}
|
|
|
|
token = token[3:]
|
|
|
|
parts := strings.SplitN(token, "::", 2)
|
|
token = parts[0]
|
|
if len(parts) > 1 {
|
|
result.caHash = parts[0]
|
|
token = parts[1]
|
|
}
|
|
|
|
parts = strings.SplitN(token, ":", 2)
|
|
if len(parts) != 2 {
|
|
return result, fmt.Errorf("token credentials are the wrong format")
|
|
}
|
|
|
|
result.username = parts[0]
|
|
result.password = parts[1]
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func GetHTTPClient(cacerts []byte) *http.Client {
|
|
if len(cacerts) == 0 {
|
|
return http.DefaultClient
|
|
}
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AppendCertsFromPEM(cacerts)
|
|
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
DisableKeepAlives: true,
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: pool,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func Get(path string, info *Info) ([]byte, error) {
|
|
u, err := url.Parse(info.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Path = path
|
|
return get(u.String(), GetHTTPClient(info.CACerts), info.username, info.password)
|
|
}
|
|
|
|
func GetCACerts(u url.URL) ([]byte, error) {
|
|
u.Path = "/cacerts"
|
|
url := u.String()
|
|
|
|
_, err := get(url, http.DefaultClient, "", "")
|
|
if err == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
cacerts, err := get(url, insecureClient, "", "")
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get CA certs at %s", url)
|
|
}
|
|
|
|
_, err = get(url, GetHTTPClient(cacerts), "", "")
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "server %s is not trusted", url)
|
|
}
|
|
|
|
return cacerts, nil
|
|
}
|
|
|
|
func get(u string, client *http.Client, username, password string) ([]byte, error) {
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if username != "" {
|
|
req.SetBasicAuth(username, password)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("%s: %s", u, resp.Status)
|
|
}
|
|
|
|
return ioutil.ReadAll(resp.Body)
|
|
}
|