Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v17] Relax validServerHostname to accept -- or .. #50410

Merged
merged 6 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1494,12 +1494,11 @@ func (a *Server) runPeriodicOperations() {
heartbeatsMissedByAuth.Inc()
}

if srv.GetSubKind() != types.SubKindOpenSSHNode {
return false, nil
}
// TODO(tross) DELETE in v20.0.0 - all invalid hostnames should have been sanitized by then.
if !validServerHostname(srv.GetHostname()) {
if srv.GetSubKind() != types.SubKindOpenSSHNode {
return false, nil
}

logger := a.logger.With("server", srv.GetName(), "hostname", srv.GetHostname())

logger.DebugContext(a.closeCtx, "sanitizing invalid static SSH server hostname")
Expand All @@ -1513,6 +1512,17 @@ func (a *Server) runPeriodicOperations() {
if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) {
logger.WarnContext(a.closeCtx, "failed to update SSH server hostname", "error", err)
}
} else if oldHostname, ok := srv.GetLabel(replacedHostnameLabel); ok && validServerHostname(oldHostname) {
// If the hostname has been replaced by a sanitized version, revert it back to the original
// if the original is valid under the most recent rules.
logger := a.logger.With("server", srv.GetName(), "old_hostname", oldHostname, "sanitized_hostname", srv.GetHostname())
if err := restoreSanitizedHostname(srv); err != nil {
logger.WarnContext(a.closeCtx, "failed to restore sanitized static SSH server hostname", "error", err)
return false, nil
}
if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) {
log.Warnf("Failed to update node hostname: %v", err)
}
}

return false, nil
Expand Down Expand Up @@ -5627,15 +5637,15 @@ func (a *Server) KeepAliveServer(ctx context.Context, h types.KeepAlive) error {

const (
serverHostnameMaxLen = 256
serverHostnameRegexPattern = `^[a-zA-Z0-9]([\.-]?[a-zA-Z0-9]+)*$`
serverHostnameRegexPattern = `^[a-zA-Z0-9]+[a-zA-Z0-9\.-]*$`
replacedHostnameLabel = types.TeleportInternalLabelPrefix + "invalid-hostname"
)

var serverHostnameRegex = regexp.MustCompile(serverHostnameRegexPattern)

// validServerHostname returns false if the hostname is longer than 256 characters or
// does not entirely consist of alphanumeric characters as well as '-' and '.'. A valid hostname also
// cannot begin with a symbol, and a symbol cannot be followed immediately by another symbol.
// cannot begin with a symbol.
func validServerHostname(hostname string) bool {
return len(hostname) <= serverHostnameMaxLen && serverHostnameRegex.MatchString(hostname)
}
Expand Down Expand Up @@ -5674,6 +5684,26 @@ func sanitizeHostname(server types.Server) error {
return nil
}

// restoreSanitizedHostname restores the original hostname of a server and removes the label.
func restoreSanitizedHostname(server types.Server) error {
oldHostname, ok := server.GetLabels()[replacedHostnameLabel]
// if the label is not present or the hostname is invalid under the most recent rules, do nothing.
if !ok || !validServerHostname(oldHostname) {
return nil
}

switch s := server.(type) {
case *types.ServerV2:
// restore the original hostname and remove the label.
s.Spec.Hostname = oldHostname
delete(s.Metadata.Labels, replacedHostnameLabel)
default:
return trace.BadParameter("invalid server provided")
}

return nil
}

// UpsertNode implements [services.Presence] by delegating to [Server.Services]
// and potentially emitting a [usagereporter] event.
func (a *Server) UpsertNode(ctx context.Context, server types.Server) (*types.KeepAlive, error) {
Expand Down
80 changes: 75 additions & 5 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4478,6 +4478,10 @@ func TestServerHostnameSanitization(t *testing.T) {
name: "uuid dns hostname",
hostname: uuid.NewString() + ".example.com",
},
{
name: "valid dns hostname with multi-dots",
hostname: "llama..example.com",
},
{
name: "empty hostname",
hostname: "",
Expand All @@ -4488,11 +4492,6 @@ func TestServerHostnameSanitization(t *testing.T) {
hostname: strings.Repeat("a", serverHostnameMaxLen*2),
invalidHostname: true,
},
{
name: "invalid dns hostname",
hostname: "llama..example.com",
invalidHostname: true,
},
{
name: "spaces in hostname",
hostname: "the quick brown fox jumps over the lazy dog",
Expand Down Expand Up @@ -4562,3 +4561,74 @@ func TestServerHostnameSanitization(t *testing.T) {
})
}
}

func TestValidServerHostname(t *testing.T) {
t.Parallel()
tests := []struct {
name string
hostname string
want bool
}{
{
name: "valid dns hostname",
hostname: "llama.example.com",
want: true,
},
{
name: "valid friendly hostname",
hostname: "llama",
want: true,
},
{
name: "uuid hostname",
hostname: uuid.NewString(),
want: true,
},
{
name: "valid hostname with multi-dashes",
hostname: "llama--example.com",
want: true,
},
{
name: "valid hostname with multi-dots",
hostname: "llama..example.com",
want: true,
},
{
name: "valid hostname with numbers",
hostname: "llama9",
want: true,
},
{
name: "hostname with invalid characters",
hostname: "llama?!$",
want: false,
},
{
name: "super long hostname",
hostname: strings.Repeat("a", serverHostnameMaxLen*2),
want: false,
},
{
name: "hostname with spaces",
hostname: "the quick brown fox jumps over the lazy dog",
want: false,
},
{
name: "hostname with ;",
hostname: "llama;example.com",
want: false,
},
{
name: "empty hostname",
hostname: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := validServerHostname(tt.hostname)
require.Equal(t, tt.want, got)
})
}
}
Loading