mirror of https://github.com/k3s-io/k3s
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
14 KiB
443 lines
14 KiB
package clientaccess |
|
|
|
import ( |
|
"crypto/tls" |
|
"crypto/x509" |
|
"encoding/pem" |
|
"net" |
|
"net/http" |
|
"net/http/httptest" |
|
"os" |
|
"testing" |
|
"time" |
|
|
|
"github.com/k3s-io/k3s/pkg/bootstrap" |
|
"github.com/k3s-io/k3s/pkg/daemons/config" |
|
"github.com/rancher/dynamiclistener/cert" |
|
"github.com/rancher/dynamiclistener/factory" |
|
"github.com/stretchr/testify/assert" |
|
) |
|
|
|
var ( |
|
defaultUsername = "server" |
|
defaultPassword = "token" |
|
defaultToken = "abcdef.0123456789abcdef" |
|
) |
|
|
|
// Test_UnitTrustedCA confirms that tokens are validated when the server uses a cert (self-signed or otherwise) |
|
// that is trusted by the OS CA bundle. This test must be run first, since it mucks with the system root certs. |
|
func Test_UnitTrustedCA(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
digest, _ := hashCA(getServerCA(server)) |
|
|
|
testInfo := &Info{ |
|
CACerts: getServerCA(server), |
|
BaseURL: server.URL, |
|
Username: defaultUsername, |
|
Password: defaultPassword, |
|
caHash: digest, |
|
} |
|
|
|
testCases := []struct { |
|
token string |
|
expected string |
|
}{ |
|
{defaultPassword, ""}, |
|
{testInfo.String(), testInfo.Username}, |
|
} |
|
|
|
// Point OS CA bundle at this test's CA cert to simulate a trusted CA cert. |
|
// Note that this only works if the OS CA bundle has not yet been loaded in this process, |
|
// as it is cached for the duration of the process lifetime. |
|
// Ref: https://github.com/golang/go/issues/41888 |
|
path := t.TempDir() + "/ca.crt" |
|
writeServerCA(server, path) |
|
os.Setenv("SSL_CERT_FILE", path) |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(server.URL, testCase.token) |
|
if assert.NoError(err, testCase) { |
|
assert.Nil(info.CACerts, testCase) |
|
assert.Equal(testCase.expected, info.Username, testCase.token) |
|
} |
|
|
|
info, err = ParseAndValidateToken(server.URL, testCase.token, WithUser("agent")) |
|
if assert.NoError(err, testCase) { |
|
assert.Nil(info.CACerts, testCase) |
|
assert.Equal("agent", info.Username, testCase) |
|
} |
|
} |
|
|
|
// Confirm that the cert is actually trusted by the OS CA bundle by making a request |
|
// with empty cert pool |
|
testInfo.CACerts = nil |
|
res, err := testInfo.Get("/v1-k3s/server-bootstrap") |
|
assert.NoError(err) |
|
assert.NotEmpty(res) |
|
} |
|
|
|
// Test_UnitUntrustedCA confirms that tokens are validated when the server uses a self-signed cert |
|
// that is NOT trusted by the OS CA bundle. |
|
func Test_UnitUntrustedCA(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
digest, _ := hashCA(getServerCA(server)) |
|
|
|
testInfo := &Info{ |
|
CACerts: getServerCA(server), |
|
BaseURL: server.URL, |
|
Username: defaultUsername, |
|
Password: defaultPassword, |
|
caHash: digest, |
|
} |
|
|
|
testCases := []struct { |
|
token string |
|
expected string |
|
}{ |
|
{defaultPassword, ""}, |
|
{testInfo.String(), testInfo.Username}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(server.URL, testCase.token) |
|
if assert.NoError(err, testCase) { |
|
assert.Equal(testInfo.CACerts, info.CACerts, testCase) |
|
assert.Equal(testCase.expected, info.Username, testCase) |
|
} |
|
|
|
info, err = ParseAndValidateToken(server.URL, testCase.token, WithUser("agent")) |
|
if assert.NoError(err, testCase) { |
|
assert.Equal(testInfo.CACerts, info.CACerts, testCase) |
|
assert.Equal("agent", info.Username, testCase) |
|
} |
|
} |
|
} |
|
|
|
// Test_UnitInvalidServers tests that invalid server URLs are properly rejected |
|
func Test_UnitInvalidServers(t *testing.T) { |
|
assert := assert.New(t) |
|
testCases := []struct { |
|
server string |
|
token string |
|
expected string |
|
}{ |
|
{" https://localhost:6443", "token", "Invalid server url, failed to parse: https://localhost:6443: parse \" https://localhost:6443\": first path segment in URL cannot contain colon"}, |
|
{"http://localhost:6443", "token", "only https:// URLs are supported, invalid scheme: http://localhost:6443"}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
_, err := ParseAndValidateToken(testCase.server, testCase.token) |
|
assert.EqualError(err, testCase.expected, testCase) |
|
|
|
_, err = ParseAndValidateToken(testCase.server, testCase.token, WithUser(defaultUsername)) |
|
assert.EqualError(err, testCase.expected, testCase) |
|
} |
|
} |
|
|
|
// Test_UnitValidTokens tests that valid tokens can be parsed, and give the expected result |
|
func Test_UnitValidTokens(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
digest, _ := hashCA(getServerCA(server)) |
|
|
|
testCases := []struct { |
|
server string |
|
token string |
|
expectUsername string |
|
expectPassword string |
|
expectToken string |
|
}{ |
|
{server.URL, defaultPassword, "", defaultPassword, ""}, |
|
{server.URL, defaultToken, "", "", defaultToken}, |
|
{server.URL, "K10" + digest + ":::" + defaultPassword, "", defaultPassword, ""}, |
|
{server.URL, "K10" + digest + "::" + defaultUsername + ":" + defaultPassword, defaultUsername, defaultPassword, ""}, |
|
{server.URL, "K10" + digest + "::" + defaultToken, "", "", defaultToken}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(testCase.server, testCase.token) |
|
assert.NoError(err) |
|
assert.Equal(testCase.expectUsername, info.Username, testCase) |
|
assert.Equal(testCase.expectPassword, info.Password, testCase) |
|
assert.Equal(testCase.expectToken, info.Token(), testCase) |
|
} |
|
} |
|
|
|
// Test_UnitInvalidTokens tests that tokens which are empty, invalid, or incorrect are properly rejected |
|
func Test_UnitInvalidTokens(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
digest, _ := hashCA(getServerCA(server)) |
|
|
|
testCases := []struct { |
|
server string |
|
token string |
|
expected string |
|
}{ |
|
{server.URL, "", "token must not be empty"}, |
|
{server.URL, "K10::", "invalid token format"}, |
|
{server.URL, "K10::x", "invalid token format"}, |
|
{server.URL, "K10::x:", "invalid token format"}, |
|
{server.URL, "K10XX::x:y", "invalid token CA hash length"}, |
|
{server.URL, |
|
"K10XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX::x:y", |
|
"token CA hash does not match the Cluster CA certificate hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX != " + digest}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(testCase.server, testCase.token) |
|
assert.EqualError(err, testCase.expected, testCase) |
|
assert.Nil(info, testCase) |
|
|
|
info, err = ParseAndValidateToken(testCase.server, testCase.token, WithUser(defaultUsername)) |
|
assert.EqualError(err, testCase.expected, testCase) |
|
assert.Nil(info, testCase) |
|
} |
|
} |
|
|
|
// Test_UnitInvalidCredentials tests that tokens which don't have valid credentials are rejected |
|
func Test_UnitInvalidCredentials(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
digest, _ := hashCA(getServerCA(server)) |
|
|
|
testInfo := &Info{ |
|
CACerts: getServerCA(server), |
|
BaseURL: server.URL, |
|
Username: "nobody", |
|
Password: "invalid", |
|
caHash: digest, |
|
} |
|
|
|
testCases := []string{ |
|
testInfo.Password, |
|
testInfo.String(), |
|
} |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(server.URL, testCase) |
|
assert.NoError(err, testCase) |
|
if assert.NotNil(info) { |
|
res, err := info.Get("/v1-k3s/server-bootstrap") |
|
assert.Error(err, testCase) |
|
assert.Empty(res, testCase) |
|
} |
|
|
|
info, err = ParseAndValidateToken(server.URL, testCase, WithUser(defaultUsername)) |
|
assert.NoError(err, testCase) |
|
if assert.NotNil(info) { |
|
res, err := info.Get("/v1-k3s/server-bootstrap") |
|
assert.Error(err, testCase) |
|
assert.Empty(res, testCase) |
|
} |
|
} |
|
} |
|
|
|
// Test_UnitWrongCert tests that errors are returned when the server's cert isn't issued by its CA |
|
func Test_UnitWrongCert(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, true) |
|
defer server.Close() |
|
|
|
info, err := ParseAndValidateToken(server.URL, defaultPassword) |
|
assert.Error(err) |
|
assert.Nil(info) |
|
|
|
info, err = ParseAndValidateToken(server.URL, defaultPassword, WithUser(defaultUsername)) |
|
assert.Error(err) |
|
assert.Nil(info) |
|
} |
|
|
|
// Test_UnitConnectionFailures tests that connections are timed out properly |
|
func Test_UnitConnectionFailures(t *testing.T) { |
|
testDuration := (defaultClientTimeout * 2) + time.Second |
|
assert := assert.New(t) |
|
testCases := []struct { |
|
server string |
|
token string |
|
}{ |
|
{"https://192.0.2.1:6443", "token"}, // RFC 5735 TEST-NET-1 for use in documentation and example code |
|
{"https://localhost:1", "token"}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
startTime := time.Now() |
|
info, err := ParseAndValidateToken(testCase.server, testCase.token) |
|
assert.Error(err, testCase) |
|
assert.Nil(info, testCase) |
|
assert.WithinDuration(time.Now(), startTime, testDuration, testCase) |
|
|
|
startTime = time.Now() |
|
info, err = ParseAndValidateToken(testCase.server, testCase.token, WithUser(defaultUsername)) |
|
assert.Error(err, testCase) |
|
assert.Nil(info, testCase) |
|
assert.WithinDuration(startTime, time.Now(), testDuration, testCase) |
|
} |
|
} |
|
|
|
// Test_UnitUserPass tests that usernames and passwords are parsed or not parsed from token strings |
|
func Test_UnitUserPass(t *testing.T) { |
|
assert := assert.New(t) |
|
testCases := []struct { |
|
token string |
|
username string |
|
password string |
|
expect bool |
|
}{ |
|
{"K10XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX::username:password", "username", "password", true}, |
|
{"password", "", "password", true}, |
|
{"K10X::x", "", "", false}, |
|
{"aaaaaa.bbbbbbbbbbbbbbbb", "", "", false}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
username, password, ok := ParseUsernamePassword(testCase.token) |
|
assert.Equal(testCase.expect, ok, testCase) |
|
if ok { |
|
assert.Equal(testCase.username, username, testCase) |
|
assert.Equal(testCase.password, password, testCase) |
|
} |
|
} |
|
} |
|
|
|
// Test_UnitParseAndGet tests URL handling along some hard-to-reach code paths |
|
func Test_UnitParseAndGet(t *testing.T) { |
|
assert := assert.New(t) |
|
server := newTLSServer(t, defaultUsername, defaultPassword, false) |
|
defer server.Close() |
|
|
|
testCases := []struct { |
|
extraBasePre string |
|
extraBasePost string |
|
path string |
|
parseFail bool |
|
getFail bool |
|
}{ |
|
{"/", "", "/cacerts", false, false}, |
|
{"/%2", "", "/cacerts", true, false}, |
|
{"", "", "/%2", false, true}, |
|
{"", "/%2", "/cacerts", false, true}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
info, err := ParseAndValidateToken(server.URL+testCase.extraBasePre, defaultPassword, WithUser(defaultUsername)) |
|
// Check for expected error when parsing server + token |
|
if testCase.parseFail { |
|
assert.Error(err, testCase) |
|
} else if assert.NoError(err, testCase) { |
|
info.BaseURL = server.URL + testCase.extraBasePost |
|
_, err := info.Get(testCase.path) |
|
// Check for expected error when making Get request |
|
if testCase.getFail { |
|
assert.Error(err, testCase) |
|
} else { |
|
assert.NoError(err, testCase) |
|
} |
|
} |
|
} |
|
} |
|
|
|
// newTLSServer returns a HTTPS server that mocks the basic functionality required to validate K3s join tokens. |
|
// Each call to this function will generate new CA and server certificates unique to the returned server. |
|
func newTLSServer(t *testing.T, username, password string, sendWrongCA bool) *httptest.Server { |
|
var server *httptest.Server |
|
server = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
if r.URL.Path == "/v1-k3s/server-bootstrap" { |
|
if authUsername, authPassword, ok := r.BasicAuth(); ok != true || authPassword != password || authUsername != username { |
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) |
|
return |
|
} |
|
bootstrapData := &config.ControlRuntimeBootstrap{} |
|
w.Header().Set("Content-Type", "application/json") |
|
if err := bootstrap.ReadFromDisk(w, bootstrapData); err != nil { |
|
t.Errorf("failed to write bootstrap: %v", err) |
|
} |
|
return |
|
} |
|
|
|
if r.URL.Path == "/cacerts" { |
|
w.Header().Set("Content-Type", "text/plain") |
|
if _, err := w.Write(getServerCA(server)); err != nil { |
|
t.Errorf("Failed to write cacerts: %v", err) |
|
} |
|
return |
|
} |
|
|
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) |
|
})) |
|
|
|
// Create new CA cert and key |
|
caCert, caKey, err := factory.GenCA() |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
|
|
// Generate new server cert; reuse the key from the CA |
|
cfg := cert.Config{ |
|
CommonName: "localhost", |
|
Organization: []string{"testing"}, |
|
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, |
|
AltNames: cert.AltNames{ |
|
DNSNames: []string{"localhost"}, |
|
IPs: []net.IP{net.IPv4(127, 0, 0, 1)}, |
|
}, |
|
} |
|
serverCert, err := cert.NewSignedCert(cfg, caKey, caCert, caKey) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
|
|
// Bind server and CA certs into chain for TLS listener configuration |
|
server.TLS = &tls.Config{} |
|
server.TLS.Certificates = []tls.Certificate{ |
|
{Certificate: [][]byte{serverCert.Raw}, Leaf: serverCert, PrivateKey: caKey}, |
|
{Certificate: [][]byte{caCert.Raw}, Leaf: caCert}, |
|
} |
|
|
|
if sendWrongCA { |
|
// Create new CA cert and key and use that as the CA cert instead of the one that actually signed the server cert |
|
badCert, _, err := factory.GenCA() |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
server.TLS.Certificates[1].Certificate[0] = badCert.Raw |
|
server.TLS.Certificates[1].Leaf = badCert |
|
} |
|
|
|
server.StartTLS() |
|
return server |
|
} |
|
|
|
// getServerCA returns a byte slice containing the PEM encoding of the server's CA certificate |
|
func getServerCA(server *httptest.Server) []byte { |
|
bytes := []byte{} |
|
for i, cert := range server.TLS.Certificates { |
|
if i == 0 { |
|
continue // Just return the chain, not the leaf server cert |
|
} |
|
bytes = append(bytes, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})...) |
|
} |
|
return bytes |
|
} |
|
|
|
// writeServerCA writes the PEM-encoded server certificate to a given path |
|
func writeServerCA(server *httptest.Server, path string) error { |
|
certOut, err := os.Create(path) |
|
if err != nil { |
|
return err |
|
} |
|
defer certOut.Close() |
|
|
|
if _, err := certOut.Write(getServerCA(server)); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
}
|
|
|