Skip to content
Draft
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
4 changes: 2 additions & 2 deletions pkg/interlink/api/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,34 @@

for _, container := range pod.Pod.Spec.InitContainers {
startContainer := time.Now().UnixMicro()
log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Docker Sidecar. InitContainer: " + container.Name)
log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Sidecar. InitContainer: " + container.Name)
log.G(ctx).Debug(container.VolumeMounts)
data, err := retrieveData(ctx, config, pod, container)
if err != nil {
log.G(ctx).Error(err)
return types.RetrievedPodData{}, err
}
retrievedData.Containers = append(retrievedData.Containers, data)

durationContainer := time.Now().UnixMicro() - startContainer
span.AddEvent("Init Container "+container.Name, trace.WithAttributes(

Check notice on line 46 in pkg/interlink/api/func.go

View workflow job for this annotation

GitHub Actions / Check duplicated code

Copy/pasted code

see pkg/interlink/api/func.go (53-63)
attribute.Int64("initcontainer.getdata.duration", durationContainer),
attribute.String("pod.name", pod.Pod.Name)))
}

for _, container := range pod.Pod.Spec.Containers {
startContainer := time.Now().UnixMicro()
log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Docker Sidecar. Container: " + container.Name)
log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Sidecar. Container: " + container.Name)
log.G(ctx).Debug(container.VolumeMounts)
data, err := retrieveData(ctx, config, pod, container)
if err != nil {
log.G(ctx).Error(err)
return types.RetrievedPodData{}, err
}
retrievedData.Containers = append(retrievedData.Containers, data)

durationContainer := time.Now().UnixMicro() - startContainer
span.AddEvent("Container "+container.Name, trace.WithAttributes(

Check notice on line 63 in pkg/interlink/api/func.go

View workflow job for this annotation

GitHub Actions / Check duplicated code

Copy/pasted code

see pkg/interlink/api/func.go (36-46)
attribute.Int64("container.getdata.duration", durationContainer),
attribute.String("pod.name", pod.Pod.Name)))
}
Expand Down
15 changes: 7 additions & 8 deletions pkg/interlink/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"io"
"net/http"
"net/url"
"strings"

"github.com/containerd/containerd/log"
"github.com/google/uuid"
Expand All @@ -21,9 +20,13 @@ import (
"github.com/interlink-hq/interlink/pkg/interlink"
)

// isSafeURL checks for SSRF by allowing only http(s) and http+unix URLs and blocking
// localhost/internal addresses for http(s). http+unix is considered safe because unix domain
// sockets are local-only and require filesystem access to connect, making remote exploitation impossible.
// isSafeURL validates that a URL uses only http or https schemes.
// It blocks non-http(s) schemes (e.g. file://, ftp://) to prevent unexpected
// protocol usage. Localhost, loopback addresses, and private IP ranges are
// intentionally allowed because the sidecar plugin routinely runs on the same
// host or on internal/private network addresses in HPC and cluster environments.
// These URLs originate from trusted operator configuration (config files),
// not from user-controlled input, so private IPs are valid.
func isSafeURL(rawurl string) bool {
u, err := url.Parse(rawurl)
if err != nil {
Expand All @@ -35,10 +38,6 @@ func isSafeURL(rawurl string) bool {
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
host := u.Hostname()
if host == "localhost" || host == "127.0.0.1" || host == "::1" || strings.HasSuffix(host, ".internal") {
return false
}
return true
}

Expand Down
75 changes: 51 additions & 24 deletions pkg/interlink/api/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"io"
"net/http"
"regexp"
"time"

"github.com/containerd/containerd/log"
Expand All @@ -17,6 +18,33 @@ import (
trace "go.opentelemetry.io/otel/trace"
)

// containerNameRegexp validates that a container name contains only safe characters.
// Kubernetes container names follow RFC 1123 subdomain rules: lowercase alphanumeric
// characters, '-', and must start and end with an alphanumeric character.
var containerNameRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*[a-z0-9]$|^[a-z0-9]$`)

// namespaceRegexp validates Kubernetes namespace names (RFC 1123 label).
var namespaceRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*[a-z0-9]$|^[a-z0-9]$`)

// podUIDRegexp validates Kubernetes pod UIDs (standard UUID format).
var podUIDRegexp = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)

// validateLogRequest checks that the log request fields are well-formed and
// safe to use in file path construction. This prevents path traversal attacks
// when the sidecar plugin builds file paths from these values.
func validateLogRequest(req types.LogStruct) error {
if !namespaceRegexp.MatchString(req.Namespace) {
return errors.New("invalid namespace: must be a valid DNS label")
}
if !podUIDRegexp.MatchString(req.PodUID) {
return errors.New("invalid pod UID: must be a valid UUID")
}
if req.ContainerName != "" && !containerNameRegexp.MatchString(req.ContainerName) {
return errors.New("invalid container name: must be a valid DNS label")
}
return nil
}

// GetLogsHandler handles HTTP GET requests to retrieve container logs.
// This endpoint streams container logs from the sidecar plugin to the client,
// supporting various log retrieval options such as tailing, following, and filtering.
Expand All @@ -30,7 +58,8 @@ import (
//
// HTTP Status Codes:
// - 200: Log retrieval successful (may be empty if no logs available)
// - 500: Internal server error (parameter conflicts, sidecar communication failures)
// - 400: Bad request (invalid or conflicting parameters)
// - 500: Internal server error (sidecar communication failures)
func (h *InterLinkHandler) GetLogsHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now().UnixMicro()
tracer := otel.Tracer("interlink-API")
Expand All @@ -44,7 +73,6 @@ func (h *InterLinkHandler) GetLogsHandler(w http.ResponseWriter, r *http.Request
sessionContext := GetSessionContext(r)
sessionContextMessage := GetSessionContextMessage(sessionContext)

var statusCode int
log.G(h.Ctx).Info(sessionContextMessage, "InterLink: received GetLogs call")
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
Expand All @@ -55,8 +83,7 @@ func (h *InterLinkHandler) GetLogsHandler(w http.ResponseWriter, r *http.Request
var req2 types.LogStruct // incoming request. To be used in interlink API. req is directly forwarded to sidecar
err = json.Unmarshal(bodyBytes, &req2)
if err != nil {
statusCode = http.StatusInternalServerError
w.WriteHeader(statusCode)
w.WriteHeader(http.StatusInternalServerError)
log.G(h.Ctx).Error(sessionContextMessage, err)
return
}
Expand All @@ -74,48 +101,48 @@ func (h *InterLinkHandler) GetLogsHandler(w http.ResponseWriter, r *http.Request
)

log.G(h.Ctx).Info(sessionContextMessage, "InterLink: new GetLogs podUID: now ", req2.PodUID)
if (req2.Opts.Tail != 0 && req2.Opts.LimitBytes != 0) || (req2.Opts.SinceSeconds != 0 && !req2.Opts.SinceTime.IsZero()) {
statusCode = http.StatusInternalServerError
w.WriteHeader(statusCode)

if req2.Opts.Tail != 0 && req2.Opts.LimitBytes != 0 {
_, err = w.Write([]byte("Both Tail and LimitBytes set. Set only one of them"))
if err != nil {
log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer"))
}
return

if err := validateLogRequest(req2); err != nil {
w.WriteHeader(http.StatusBadRequest)
if _, werr := w.Write([]byte(err.Error())); werr != nil {
log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer"))
}
return
}

_, err = w.Write([]byte("Both SinceSeconds and SinceTime set. Set only one of them"))
if err != nil {
if req2.Opts.Tail != 0 && req2.Opts.LimitBytes != 0 {
w.WriteHeader(http.StatusBadRequest)
if _, werr := w.Write([]byte("Both Tail and LimitBytes set. Set only one of them")); werr != nil {
log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer"))
}
return
}

if req2.Opts.SinceSeconds != 0 && !req2.Opts.SinceTime.IsZero() {
w.WriteHeader(http.StatusBadRequest)
if _, werr := w.Write([]byte("Both SinceSeconds and SinceTime set. Set only one of them")); werr != nil {
log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer"))
}
return
}

log.G(h.Ctx).Info(sessionContextMessage, "InterLink: marshal GetLogs request ")

bodyBytes, err = json.Marshal(req2)
if err != nil {
statusCode = http.StatusInternalServerError
w.WriteHeader(statusCode)
w.WriteHeader(http.StatusInternalServerError)
log.G(h.Ctx).Error(err)
return
}
reader := bytes.NewReader(bodyBytes)
log.G(h.Ctx).Info("Sending log request to: ", h.SidecarEndpoint)
req, err := http.NewRequest(http.MethodGet, h.SidecarEndpoint+"/getLogs", reader)
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, h.SidecarEndpoint+"/getLogs", reader)
if err != nil {
log.G(h.Ctx).Fatal(err)
}

req.Header.Set("Content-Type", "application/json")

// logTransport := http.DefaultTransport.(*http.Transport).Clone()
// // logTransport.DisableKeepAlives = true
// // logTransport.MaxIdleConnsPerHost = -1
// var logHTTPClient = &http.Client{Transport: logTransport}

log.G(h.Ctx).Info(sessionContextMessage, "InterLink: forwarding GetLogs call to sidecar")
_, err = ReqWithError(h.Ctx, req, w, start, span, true, false, sessionContext, h.ClientHTTP)
if err != nil {
Expand Down
Loading
Loading