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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Run Helm unit tests
run: ./eoapi-cli test unit

- name: Check container images for root user
run: ./eoapi-cli test images

integration-tests:
name: Integration tests
needs: fast-checks
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Ensured non-root container images [#382](https://github.com/developmentseed/eoapi-k8s/pull/382)

### Fixed

- Fixed Helm template to check queryables `file` field with schema validation [#380](https://github.com/developmentseed/eoapi-k8s/pull/380)
Expand Down
2 changes: 1 addition & 1 deletion charts/eoapi/templates/_helpers/services.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Helper function for common init containers to wait for pgstac jobs
{{- if .Values.pgstacBootstrap.enabled }}
initContainers:
- name: wait-for-pgstac-jobs
image: alpine/k8s:1.28.0
image: bitnami/kubectl:latest
env:
{{- include "eoapi.commonEnvVars" (dict "service" "init" "root" .) | nindent 2 }}
resources:
Expand Down
6 changes: 3 additions & 3 deletions charts/eoapi/templates/services/doc-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ spec:
spec:
containers:
- name: doc-server
image: nginx:alpine
image: nginxinc/nginx-unprivileged:alpine
volumeMounts:
- name: {{ .Release.Name }}-doc-html
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80
- containerPort: 8080
volumes:
- name: {{ .Release.Name }}-doc-html
configMap:
Expand All @@ -71,6 +71,6 @@ spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
targetPort: 8080
---
{{- end }}
15 changes: 14 additions & 1 deletion scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ COMMANDS:
schema Validate Helm chart schema
lint Run Helm lint on chart
unit Run Helm unit tests
images Check container images for root user
integration Run integration tests with pytest
notification Run notification tests with database access
autoscaling Run autoscaling tests with pytest
Expand Down Expand Up @@ -115,6 +116,14 @@ test_unit() {
fi
}

test_images() {
log_info "Checking container images for root user..."

check_requirements docker helm || return 1

"${SCRIPT_DIR}/test/images.sh"
}

test_integration() {
local pytest_args="${1:-}"
export NAMESPACE="$NAMESPACE"
Expand Down Expand Up @@ -147,6 +156,7 @@ test_all() {
test_schema || ((failed++))
test_lint || ((failed++))
test_unit || ((failed++))
test_images || ((failed++))

if validate_cluster 2>/dev/null; then
test_integration || ((failed++))
Expand Down Expand Up @@ -192,7 +202,7 @@ main() {
pytest_args="$2"
shift 2
;;
schema|lint|unit|notification|integration|autoscaling|all)
schema|lint|unit|images|notification|integration|autoscaling|all)
command="$1"
shift
break
Expand All @@ -217,6 +227,9 @@ main() {
unit)
test_unit
;;
images)
test_images
;;
integration)
test_integration "$pytest_args"
;;
Expand Down
149 changes: 149 additions & 0 deletions scripts/test/images.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env bash

# eoAPI Container Image Root User Check

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"

source "${SCRIPT_DIR}/../lib/common.sh"

CHART_PATH="${PROJECT_ROOT}/charts/eoapi"
PROFILE_PATH="${PROJECT_ROOT}/charts/eoapi/profiles/experimental.yaml"

echo "======================================"
echo "Container Image Root User Audit"
echo "======================================"
echo ""

# Extract images from Helm templates
if ! command -v helm &>/dev/null; then
log_error "helm is required but not installed"
exit 1
fi

# Extract images, excluding testing-only images
# Filters out: Helm test hooks, mock/sample/test images
if [[ ! -f "$PROFILE_PATH" ]]; then
log_error "Experimental profile not found: $PROFILE_PATH"
exit 1
fi

# Update Helm dependencies if needed
log_debug "Updating Helm chart dependencies..."
if ! helm dependency update "$CHART_PATH" &>/dev/null; then
log_warn "Helm dependency update failed, continuing anyway..."
fi

rendered_yaml=$(helm template test-release "$CHART_PATH" \
--set gitSha=test \
-f "$PROFILE_PATH" \
--set stac-auth-proxy.enabled=false \
2>&1 || \
helm template test-release "$CHART_PATH" \
--set gitSha=test \
-f "$PROFILE_PATH" \
--set stac-auth-proxy.env.OIDC_DISCOVERY_URL=https://dummy.example.com/.well-known/openid-configuration \
2>&1)

if [[ -z "$rendered_yaml" ]] || echo "$rendered_yaml" | grep -q "Error:"; then
log_error "Failed to render Helm templates"
echo "$rendered_yaml" | head -20
exit 1
fi

images=()
while IFS= read -r line; do
[[ -n "$line" ]] && images+=("$line")
done < <(
# Extract images with context to identify test hooks
echo "$rendered_yaml" | awk '
BEGIN { in_test_hook = 0 }
/^---/ { in_test_hook = 0 }
/helm\.sh\/hook.*test/ { in_test_hook = 1 }
/^\s+(- )?image:/ {
image = $0
gsub(/.*image:\s*/, "", image)
gsub(/["'\''"]/, "", image)
gsub(/^[[:space:]]+/, "", image)
if (image && image != "") {
# Skip if in test hook
if (!in_test_hook) {
# Skip images with testing patterns (but allow "test-release" in image names)
if (image !~ /\/mock/ &&
image !~ /\/sample/ &&
image !~ /\/bats\// &&
image !~ /mock-/ &&
image !~ /-mock/ &&
image !~ /sample/ &&
image !~ /bats:/) {
print image
}
}
}
}
' | sort -u
)

if [[ ${#images[@]} -eq 0 ]]; then
log_error "No images found in Helm templates"
log_info "Rendered YAML length: ${#rendered_yaml} characters"
exit 1
fi

log_debug "Found ${#images[@]} images to check"

total=0
root_count=0
non_root_count=0
error_count=0

check_image() {
local image=$1
local user

echo -n "Checking: $image ... "

if docker pull "$image" &>/dev/null; then
if ! user=$(docker inspect "$image" --format='{{.Config.User}}' 2>/dev/null); then
echo -e "${RED}ERROR${NC} (Failed to inspect)"
((error_count++))
return
fi

if [ -z "$user" ] || [ "$user" == "0" ] || [ "$user" == "root" ] || [ "$user" == "0:0" ]; then
echo -e "${RED}⚠️ RUNS AS ROOT${NC} (User: ${user:-not set})"
((root_count++))
else
echo -e "${GREEN}✓ Non-root${NC} (User: $user)"
((non_root_count++))
fi
else
echo -e "${YELLOW}SKIP${NC} (Failed to pull image)"
((error_count++))
fi
}

for image in "${images[@]}"; do
check_image "$image" || true
((total++)) || true
done

echo ""
echo "======================================"
echo "Summary"
echo "======================================"
echo "Total images checked: $total"
echo -e "${RED}Running as root: $root_count${NC}"
echo -e "${GREEN}Running as non-root: $non_root_count${NC}"
echo -e "${YELLOW}Errors/Skipped: $error_count${NC}"
echo ""

if [ $root_count -gt 0 ]; then
echo -e "${RED}⚠️ WARNING: $root_count image(s) run as root user${NC}"
exit 1
else
echo -e "${GREEN}✓ All images run as non-root user${NC}"
exit 0
fi
Loading