Skip to content
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
23 changes: 10 additions & 13 deletions caddy/Caddyfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,21 @@ oullin.io {

# INTERNAL mTLS entrypoint for the single protected path
:8443 {
tls internal {
tls /etc/caddy/mtls/server.pem /etc/caddy/mtls/server.key {
client_auth {
mode require_and_verify
trust_pool file {
pem_file /etc/caddy/mtls/ca.pem
}
# The client must present a cert trusted by the CA
trust_pool file /etc/caddy/mtls/ca.pem
}
}

Comment on lines +92 to 99
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Blocking issue: server.pem must have SANs matching the SNI clients use.

Your Makefile generates a server cert with only CN=oullin_proxy_prod and no subjectAltName. Modern clients (Chrome 58+, Go 1.15+) ignore CN for hostname verification; without SANs, the TLS handshake will fail even before HTTP routing/mTLS verification. See my Makefile comment with a patch to add SAN/EKU. (alexanderzeitler.com)

🤖 Prompt for AI Agents
In caddy/Caddyfile.prod around lines 92 to 99, the server certificate file
server.pem was created with only a CN and no subjectAltName (SAN), which modern
TLS clients ignore for hostname verification; update the Makefile target that
generates server.pem to include proper SANs (DNS names and any IPs) that match
the SNI hostnames clients use and ensure the certificate includes the serverAuth
EKU; implement this by adding a custom OpenSSL config or command-line flags to
inject subjectAltName and extendedKeyUsage when creating/signing the server cert
(or reissue the cert via the CA), then regenerate server.pem and server.key and
verify the SANs match the service hostnames used by clients.

encode gzip zstd
encode gzip zstd

# Only this path is reachable here
handle_path /api/generate-signature* {
reverse_proxy api:8080
}
handle_path /api/generate-signature* {
reverse_proxy api:8080
}

# Everything else is denied on this listener.
handle {
respond 403
}
handle {
respond 403
}
}
21 changes: 0 additions & 21 deletions metal/kernel/router_signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,4 @@ func TestSignatureRoute_PublicMiddleware(t *testing.T) {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
})

t.Run("production rejects requests from non-whitelisted IP", func(t *testing.T) {
r := Router{
Mux: http.NewServeMux(),
Pipeline: middleware.Pipeline{
PublicMiddleware: middleware.MakePublicMiddleware("31.97.60.190", true),
},
validator: portal.GetDefaultValidator(),
}
r.Signature()

req := httptest.NewRequest("POST", "/generate-signature", strings.NewReader("{"))
req.Header.Set(portal.RequestIDHeader, "req-1")
req.Header.Set(portal.TimestampHeader, fmt.Sprintf("%d", time.Now().Unix()))
req.Header.Set("X-Forwarded-For", "1.2.3.4")
rec := httptest.NewRecorder()
r.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
})
}
48 changes: 38 additions & 10 deletions metal/makefile/caddy.mk
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
.PHONY: caddy-gen-certs caddy-del-certs caddy-validate
.PHONY: caddy-gen-certs caddy-del-certs caddy-validate caddy-fresh

CADDY_MTLS_DIR = $(ROOT_PATH)/caddy/mtls
APP_CADDY_CONFIG_PROD_FILE ?= caddy/Caddyfile.prod
APP_CADDY_CONFIG_LOCAL_FILE ?= caddy/Caddyfile.local

caddy-fresh:
@make caddy-del-certs
@make caddy-gen-certs

caddy-gen-certs:
@set -eu; \
mkdir -p "$(CADDY_MTLS_DIR)"; chmod 700 "$(CADDY_MTLS_DIR)"; \
if [ -d "$(CADDY_MTLS_DIR)/ca.pem" ]; then \
printf "$(RED)✘ ERROR:$(NC) %s is a directory. Move or remove it.\n" "$(CADDY_MTLS_DIR)/ca.pem"; \
exit 1; \
fi; \
if [ -e "$(CADDY_MTLS_DIR)/ca.key" ] || [ -e "$(CADDY_MTLS_DIR)/ca.pem" ]; then \
printf "$(YELLOW)⚠️ CA already exists in %s.$(NC)\n" "$(CADDY_MTLS_DIR)"; \
printf "$(CYAN)👉 Remove it with 'make caddy-clean-certs' if you want to recreate.$(NC)\n"; \
printf "$(CYAN)👉 Remove it with 'make caddy-del-certs' if you want to recreate.$(NC)\n"; \
else \
umask 077; \
printf "$(BLUE)🔑 Generating CA private key...$(NC)\n"; \
openssl genrsa -out "$(CADDY_MTLS_DIR)/ca.key" 4096 >/dev/null 2>&1; \
openssl genrsa -out "$(CADDY_MTLS_DIR)/ca.key" 4096; \
printf "$(BLUE)📜 Creating self-signed CA certificate...$(NC)\n"; \
openssl req -x509 -new -key "$(CADDY_MTLS_DIR)/ca.key" -sha256 -days 3650 \
-subj "/CN=oullin-mtls-ca" -out "$(CADDY_MTLS_DIR)/ca.pem" >/dev/null 2>&1; \
-subj "/CN=oullin-mtls-ca" -out "$(CADDY_MTLS_DIR)/ca.pem"; \
printf '01\n' > "$(CADDY_MTLS_DIR)/ca.srl"; \
chmod 600 "$(CADDY_MTLS_DIR)/ca.key"; \
chmod 644 "$(CADDY_MTLS_DIR)/ca.pem" "$(CADDY_MTLS_DIR)/ca.srl"; \
printf "$(GREEN)✅ CA written to %s$(NC)\n" "$(CADDY_MTLS_DIR)"; \
printf "$(WHITE)🔍 CA fingerprint:$(NC)\n"; \
openssl x509 -noout -fingerprint -sha256 -in "$(CADDY_MTLS_DIR)/ca.pem" | sed 's/^/ /'; \
fi
fi; \
if [ -e "$(CADDY_MTLS_DIR)/server.key" ] || [ -e "$(CADDY_MTLS_DIR)/server.pem" ]; then \
printf "$(YELLOW)⚠️ Server certificate already exists.$(NC)\n"; \
else \
umask 077; \
printf "$(BLUE)🔑 Generating Server private key...$(NC)\n"; \
openssl genrsa -out "$(CADDY_MTLS_DIR)/server.key" 4096; \
printf "$(WHITE)📜 Creating Server signing request...$(NC)\n"; \
openssl req -new -key "$(CADDY_MTLS_DIR)/server.key" -subj "/CN=oullin_proxy_prod" -out "$(CADDY_MTLS_DIR)/server.csr"; \
printf "$(NC)🖊️ Signing Server certificate with CA...$(NC)\n"; \
openssl x509 -req -in "$(CADDY_MTLS_DIR)/server.csr" \
-CA "$(CADDY_MTLS_DIR)/ca.pem" -CAkey "$(CADDY_MTLS_DIR)/ca.key" -CAserial "$(CADDY_MTLS_DIR)/ca.srl" \
-out "$(CADDY_MTLS_DIR)/server.pem" -days 1095 -sha256; \
chmod 600 "$(CADDY_MTLS_DIR)/server.key"; \
chmod 644 "$(CADDY_MTLS_DIR)/server.pem"; \
rm "$(CADDY_MTLS_DIR)/server.csr"; \
printf "$(GREEN)✅ Server certificate written to %s$(NC)\n" "$(CADDY_MTLS_DIR)"; \
fi; \
printf "$(NC)🔎 Verifying server cert against CA...$(NC)\n"; \
openssl verify -CAfile "$(CADDY_MTLS_DIR)/ca.pem" "$(CADDY_MTLS_DIR)/server.pem"

caddy-del-certs:
@set -eu; \
rm -f "$(CADDY_MTLS_DIR)/ca.key" "$(CADDY_MTLS_DIR)/ca.pem" "$(CADDY_MTLS_DIR)/ca.srl"; \
rm -f "$(CADDY_MTLS_DIR)/ca.key" \
"$(CADDY_MTLS_DIR)/ca.pem" \
"$(CADDY_MTLS_DIR)/ca.srl" \
"$(CADDY_MTLS_DIR)/server.key" \
"$(CADDY_MTLS_DIR)/server.pem"; \
printf "$(BLUE)✅ files removed from [$(NC)$(CADDY_MTLS_DIR)$(BLUE)]$(NC)\n"

caddy-validate:
docker run --rm \
-v "$(ROOT_PATH)/caddy/Caddyfile.prod:/etc/caddy/Caddyfile:ro" \
-v "$(ROOT_PATH)/caddy/mtls:/etc/caddy/mtls:ro" \
caddy:2.10.0 caddy validate --config /etc/caddy/Caddyfile
@docker run --rm \
-v "$(ROOT_PATH)/caddy/Caddyfile.prod:/etc/caddy/Caddyfile:ro" \
-v "$(ROOT_PATH)/caddy/mtls:/etc/caddy/mtls:ro" \
caddy:2.10.0 caddy validate --config /etc/caddy/Caddyfile
20 changes: 0 additions & 20 deletions pkg/middleware/public_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ func (p PublicMiddleware) Handle(next http.ApiHandler) http.ApiHandler {
return mwguards.RateLimitedError("Too many requests", "Too many requests for key: "+limiterKey)
}

if err := p.HasInvalidIP(r); err != nil {
p.rateLimiter.Fail(limiterKey)

return err
}

vt := mwguards.NewValidTimestamp(ts, p.now)
if err := vt.Validate(p.clockSkew, p.disallowFuture); err != nil {
p.rateLimiter.Fail(limiterKey)
Expand All @@ -89,20 +83,6 @@ func (p PublicMiddleware) Handle(next http.ApiHandler) http.ApiHandler {
}
}

func (p PublicMiddleware) HasInvalidIP(r *baseHttp.Request) *http.ApiError {
ip := portal.ParseClientIP(r)

if ip == "" {
return mwguards.InvalidRequestError("Clients IPs are required to access this endpoint", "")
}

if p.isProduction && ip != p.allowedIP {
return mwguards.InvalidRequestError("The given IP is not allowed", "unauthorised ip: "+ip)
}

return nil
}

func (p PublicMiddleware) GuardDependencies() *http.ApiError {
missing := []string{}

Expand Down
Loading