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

feat: support pod exec terminal logging #9385

Merged
merged 7 commits into from
May 17, 2022
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
2 changes: 1 addition & 1 deletion applicationset/generators/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (g *ClusterGenerator) getSecretsByClusterName(appSetGenerator *argoappsetv1

}

// santize the name in accordance with the below rules
// sanitize the name in accordance with the below rules
// 1. contain no more than 253 characters
// 2. contain only lowercase alphanumeric characters, '-' or '.'
// 3. start and end with an alphanumeric character
Expand Down
2 changes: 1 addition & 1 deletion server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var (
watchAPIBufferSize = env.ParseNumFromEnv(argocommon.EnvWatchAPIBufferSize, 1000, 0, math.MaxInt32)
)

// Server provides a Application service
// Server provides an Application service
type Server struct {
ns string
kubeclientset kubernetes.Interface
Expand Down
64 changes: 59 additions & 5 deletions server/application/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import (
"context"
"io"
"net/http"
"net/url"
"strings"

"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
Expand All @@ -21,6 +26,7 @@ import (
"github.com/argoproj/argo-cd/v2/util/argo"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/rbac"
sessionmgr "github.com/argoproj/argo-cd/v2/util/session"
)

type terminalHandler struct {
Expand Down Expand Up @@ -54,26 +60,70 @@ func (s *terminalHandler) getApplicationClusterRawConfig(ctx context.Context, a
return clst.RawRestConfig(), nil
}

// isValidPodName checks that a podName is valid
func isValidPodName(name string) bool {
// https://github.com/kubernetes/kubernetes/blob/976a940f4a4e84fe814583848f97b9aafcdb083f/pkg/apis/core/validation/validation.go#L241
isValid := apimachineryvalidation.NameIsDNSSubdomain(name, false)
return len(isValid) == 0
}

// isValidNamespaceName checks that a namespace name is valid
func isValidNamespaceName(name string) bool {
// https://github.com/kubernetes/kubernetes/blob/976a940f4a4e84fe814583848f97b9aafcdb083f/pkg/apis/core/validation/validation.go#L262
isValid := apimachineryvalidation.ValidateNamespaceName(name, false)
return len(isValid) == 0
}

// isValidContainerName checks that a containerName is valid
func isValidContainerName(name string) bool {
// quick check to ensure we aren't passing along anything unsafe
isQualified := validation.IsQualifiedName(name)
smcavallo marked this conversation as resolved.
Show resolved Hide resolved
return len(isQualified) == 0
}

// GetQueryValue returns a value for a given url key
// and strips newline to prevent go/log-injection
func GetQueryValue(q url.Values, key string) string {
return strings.Replace(q.Get(key), "\n", "", -1)
smcavallo marked this conversation as resolved.
Show resolved Hide resolved
}

func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
podName := q.Get("pod")
container := q.Get("container")
app := q.Get("appName")
namespace := q.Get("namespace")
shell := q.Get("shell")
podName := GetQueryValue(q, "pod")
container := GetQueryValue(q, "container")
app := GetQueryValue(q, "appName")
namespace := GetQueryValue(q, "namespace")
shell := GetQueryValue(q, "shell")

if podName == "" || container == "" || app == "" || namespace == "" {
http.Error(w, "Missing required parameters", http.StatusBadRequest)
return
}

// validate query string parameters to prevent unsafe usage
if !isValidNamespaceName(namespace) {
http.Error(w, "Namespace name is not valid", http.StatusBadRequest)
return
}
if !isValidPodName(podName) {
http.Error(w, "Pod name is not valid", http.StatusBadRequest)
return
}
if !isValidContainerName(container) {
http.Error(w, "Container name is not valid", http.StatusBadRequest)
return
}

ctx := r.Context()
a, err := s.appLister.Get(app)
if err != nil {
http.Error(w, "Cannot get app", http.StatusBadRequest)
return
}

fieldLog := log.WithFields(log.Fields{"userName": sessionmgr.Username(ctx), "container": container,
"podName": podName, "namespace": namespace, "cluster": a.Spec.Destination.Name})

appRBACName := apputil.AppRBACName(*a)
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, appRBACName); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
Expand Down Expand Up @@ -111,6 +161,7 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

pod, err := kubeClientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
fieldLog.Errorf("error retrieving pod: %s", err)
http.Error(w, "Cannot find pod", http.StatusBadRequest)
return
}
Expand All @@ -128,10 +179,13 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if !findContainer {
fieldLog.Warn("terminal container not found")
http.Error(w, "Cannot find container", http.StatusBadRequest)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For auditing it would be interesting to also have this logged (as a warning) with all info: cluster, namespace, pod name and container name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL might complain about logging the un-sanitized container input. But since we're validating the container name above, I think it would be safe to log (and to override the CodeQL warning).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leoluz - yes this is the issue we were trying to avoid - Log entries created from user input High This log write receives unsanitized user input from here.
It is unsafe to log verbatim whatever was posted to URL params which is why it is not logged.
In theory it should rarely happen as most requests should come directly from the argocd application itself and only post namespace + pod + container as already found in argocd. I agree it would be useful to be able to debug though. If a user really wants to know why it can't be found wondering if there is wireshark/tcpdump some level of capturing the http request instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unsafe to log verbatim whatever was posted to URL params which is why it is not logged.

@smcavallo but you are previously sanitizing isn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sanitizing, but validating. Which I think should be enough.

If we're reaching this point of the code, then we've shown the user is authenticated and authorized to get the application and create on the exec resource. I'm not too worried about this user filling up the disk with this log line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leoluz and @crenshaw-dev - totally make sense - we've already validated so it's OK to log these. I have added the additional info to these logs.

return
}

fieldLog.Info("terminal session starting")

session, err := newTerminalSession(w, r, nil)
if err != nil {
http.Error(w, "Failed to start terminal session", http.StatusBadRequest)
Expand Down
98 changes: 98 additions & 0 deletions server/application/terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,101 @@ func TestPodExists(t *testing.T) {
})
}
}

func TestIsValidPodName(t *testing.T) {
for _, tcase := range []struct {
name string
resourceName string
expectedResult bool
}{
{
name: "valid pod name",
resourceName: "argocd-server-794644486d-r8v9d",
expectedResult: true,
},
{
name: "not valid contains spaces",
resourceName: "kubectl delete pods",
expectedResult: false,
},
{
name: "not valid",
resourceName: "kubectl -n kube-system delete pods --all",
expectedResult: false,
},
{
name: "not valid contains special characters",
resourceName: "delete+*+from+etcd%3b",
expectedResult: false,
},
} {
t.Run(tcase.name, func(t *testing.T) {
result := isValidPodName(tcase.resourceName)
if result != tcase.expectedResult {
t.Errorf("Expected result %v, but got %v", tcase.expectedResult, result)
}
})
}
}

func TestIsValidNamespaceName(t *testing.T) {
for _, tcase := range []struct {
name string
resourceName string
expectedResult bool
}{
{
name: "valid pod namespace name",
resourceName: "argocd",
expectedResult: true,
},
{
name: "not valid contains spaces",
resourceName: "kubectl delete ns argocd",
expectedResult: false,
},
{
name: "not valid contains special characters",
resourceName: "delete+*+from+etcd%3b",
expectedResult: false,
},
} {
t.Run(tcase.name, func(t *testing.T) {
result := isValidNamespaceName(tcase.resourceName)
if result != tcase.expectedResult {
t.Errorf("Expected result %v, but got %v", tcase.expectedResult, result)
}
})
}
}

func TestIsValidContainerNameName(t *testing.T) {
for _, tcase := range []struct {
name string
resourceName string
expectedResult bool
}{
{
name: "valid container name",
resourceName: "argocd-server",
expectedResult: true,
},
{
name: "not valid contains spaces",
resourceName: "kubectl delete pods",
expectedResult: false,
},
{
name: "not valid contains special characters",
resourceName: "delete+*+from+etcd%3b",
expectedResult: false,
},
} {
t.Run(tcase.name, func(t *testing.T) {
result := isValidContainerName(tcase.resourceName)
if result != tcase.expectedResult {
t.Errorf("Expected result %v, but got %v", tcase.expectedResult, result)
}
})
}
}