fix: prevent loopback Run release URLs
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
# Copy to .env if you want docker compose to read custom values automatically.
|
# Copy to .env if you want docker compose to read custom values automatically.
|
||||||
|
|
||||||
PLATFORM_ADDR=:8080
|
PLATFORM_ADDR=:8080
|
||||||
|
# Public Platform URL compiled into generated Run packages. It must be reachable from the target server.
|
||||||
|
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com/
|
||||||
|
|
||||||
# Platform metadata storage backend:
|
# Platform metadata storage backend:
|
||||||
# - file: default, writes platform metadata JSON to PLATFORM_METADATA_PATH.
|
# - file: default, writes platform metadata JSON to PLATFORM_METADATA_PATH.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
PLATFORM_LOG_DIR: /data/platform/logs
|
PLATFORM_LOG_DIR: /data/platform/logs
|
||||||
PLATFORM_ARTIFACT_DIR: /data/platform/artifacts
|
PLATFORM_ARTIFACT_DIR: /data/platform/artifacts
|
||||||
PLATFORM_SECRET_ENVELOPE_KEY: ${PLATFORM_SECRET_ENVELOPE_KEY:-local-compose-secret-envelope-key-change-me}
|
PLATFORM_SECRET_ENVELOPE_KEY: ${PLATFORM_SECRET_ENVELOPE_KEY:-local-compose-secret-envelope-key-change-me}
|
||||||
|
PLATFORM_RUN_RELEASE_URL: ${PLATFORM_RUN_RELEASE_URL:-https://scum.npc0.com/}
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ PLATFORM_BUILDER_SOURCE_REPOSITORY=git@git.npc0.com:admin343/run.git
|
|||||||
PLATFORM_BUILDER_SOURCE_REVISION=main
|
PLATFORM_BUILDER_SOURCE_REVISION=main
|
||||||
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
|
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
|
||||||
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
|
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
|
||||||
# URL embedded into generated Run packages; override for tunnel or production access.
|
# URL embedded into generated Run packages. It must be reachable from the target server.
|
||||||
PLATFORM_RUN_RELEASE_URL=http://127.0.0.1:8080
|
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com/
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ Runtime configuration:
|
|||||||
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
|
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
|
||||||
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
|
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
|
||||||
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
|
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
|
||||||
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `http://127.0.0.1:8080/`; set this explicitly for tunnel or production access.
|
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `https://scum.npc0.com/`. It must be reachable from the target server; loopback and unspecified addresses are rejected.
|
||||||
|
|
||||||
Build the dedicated toolchain image before enabling distribution generation:
|
Build the dedicated toolchain image before enabling distribution generation:
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ func (svc *CoreService) executeDistributionBuild(job domain.Job) error {
|
|||||||
// component auth key, inside the platform. Unlike GetDistributionBuildInput it
|
// component auth key, inside the platform. Unlike GetDistributionBuildInput it
|
||||||
// never crosses the job channel.
|
// never crosses the job channel.
|
||||||
func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.DistributionBuildInput, error) {
|
func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.DistributionBuildInput, error) {
|
||||||
|
platformURL, err := runReleasePlatformURL()
|
||||||
|
if err != nil {
|
||||||
|
return domain.DistributionBuildInput{}, err
|
||||||
|
}
|
||||||
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.DistributionBuildInput{}, err
|
return domain.DistributionBuildInput{}, err
|
||||||
@@ -173,7 +177,7 @@ func (svc *CoreService) platformDistributionBuildInput(job domain.Job) (domain.D
|
|||||||
TargetOS: distribution.TargetOS,
|
TargetOS: distribution.TargetOS,
|
||||||
TargetArch: distribution.TargetArch,
|
TargetArch: distribution.TargetArch,
|
||||||
TargetRelease: distribution.ID,
|
TargetRelease: distribution.ID,
|
||||||
PlatformURL: runReleasePlatformURL(),
|
PlatformURL: platformURL,
|
||||||
PackageFormat: distribution.PackageFormat,
|
PackageFormat: distribution.PackageFormat,
|
||||||
ArtifactID: distribution.ArtifactID,
|
ArtifactID: distribution.ArtifactID,
|
||||||
OutputFilename: executableFilename("run", distribution.TargetOS),
|
OutputFilename: executableFilename("run", distribution.TargetOS),
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
|
|||||||
if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
|
if job.State != domain.JobStateAccepted && job.State != domain.JobStateRunning {
|
||||||
return domain.DistributionBuildInput{}, validationError("distribution build job is not active")
|
return domain.DistributionBuildInput{}, validationError("distribution build job is not active")
|
||||||
}
|
}
|
||||||
|
platformURL, err := runReleasePlatformURL()
|
||||||
|
if err != nil {
|
||||||
|
return domain.DistributionBuildInput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
runDistributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: job.ServerInstanceID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,7 +68,7 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
|
|||||||
TargetOS: distribution.TargetOS,
|
TargetOS: distribution.TargetOS,
|
||||||
TargetArch: distribution.TargetArch,
|
TargetArch: distribution.TargetArch,
|
||||||
TargetRelease: distribution.ID,
|
TargetRelease: distribution.ID,
|
||||||
PlatformURL: runReleasePlatformURL(),
|
PlatformURL: platformURL,
|
||||||
PackageFormat: distribution.PackageFormat,
|
PackageFormat: distribution.PackageFormat,
|
||||||
ArtifactID: distribution.ArtifactID,
|
ArtifactID: distribution.ArtifactID,
|
||||||
OutputFilename: executableFilename("run", distribution.TargetOS),
|
OutputFilename: executableFilename("run", distribution.TargetOS),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -48,6 +50,9 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
|
|||||||
if ready, reason := svc.distributionBuilderReadiness(); !ready {
|
if ready, reason := svc.distributionBuilderReadiness(); !ready {
|
||||||
return domain.RunDistribution{}, validationError(reason)
|
return domain.RunDistribution{}, validationError(reason)
|
||||||
}
|
}
|
||||||
|
if _, err := runReleasePlatformURL(); err != nil {
|
||||||
|
return domain.RunDistribution{}, err
|
||||||
|
}
|
||||||
|
|
||||||
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
key, _, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -778,11 +783,28 @@ func packageFormatForTarget(targetOS string) string {
|
|||||||
return "tar.gz"
|
return "tar.gz"
|
||||||
}
|
}
|
||||||
|
|
||||||
func runReleasePlatformURL() string {
|
const defaultRunReleasePlatformURL = "https://scum.npc0.com/"
|
||||||
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
|
|
||||||
return value
|
func runReleasePlatformURL() (string, error) {
|
||||||
|
value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL"))
|
||||||
|
if value == "" {
|
||||||
|
value = defaultRunReleasePlatformURL
|
||||||
}
|
}
|
||||||
return "http://127.0.0.1:8080/"
|
parsed, err := url.ParseRequestURI(value)
|
||||||
|
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" || strings.Contains(value, "#") {
|
||||||
|
return "", validationError("PLATFORM_RUN_RELEASE_URL must be an absolute http or https URL without credentials, query parameters, or fragments")
|
||||||
|
}
|
||||||
|
host := strings.TrimRight(strings.ToLower(parsed.Hostname()), ".")
|
||||||
|
if host == "" {
|
||||||
|
return "", validationError("PLATFORM_RUN_RELEASE_URL must be an absolute http or https URL with a host")
|
||||||
|
}
|
||||||
|
if host == "localhost" || strings.HasSuffix(host, ".localhost") {
|
||||||
|
return "", validationError("PLATFORM_RUN_RELEASE_URL must use an address reachable from the target server; loopback or unspecified addresses are not valid for generated Run packages")
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsUnspecified()) {
|
||||||
|
return "", validationError("PLATFORM_RUN_RELEASE_URL must use an address reachable from the target server; loopback or unspecified addresses are not valid for generated Run packages")
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func distributionID(prefix string, parts ...interface{}) string {
|
func distributionID(prefix string, parts ...interface{}) string {
|
||||||
|
|||||||
@@ -23,6 +23,94 @@ type generatedPackageConfig struct {
|
|||||||
AuthKey string
|
AuthKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunReleasePlatformURLUsesExternalDefault(t *testing.T) {
|
||||||
|
t.Setenv("PLATFORM_RUN_RELEASE_URL", "")
|
||||||
|
|
||||||
|
value, err := runReleasePlatformURL()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve default Run release URL: %v", err)
|
||||||
|
}
|
||||||
|
if value != defaultRunReleasePlatformURL {
|
||||||
|
t.Fatalf("unexpected default Run release URL %q", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunReleasePlatformURLRejectsTargetLocalAddresses(t *testing.T) {
|
||||||
|
for _, value := range []string{
|
||||||
|
"http://127.0.0.1:18080",
|
||||||
|
"http://127.0.0.2:18080",
|
||||||
|
"http://localhost:18080",
|
||||||
|
"https://platform.localhost/",
|
||||||
|
"http://[::1]:18080",
|
||||||
|
"http://0.0.0.0:18080",
|
||||||
|
"http://[::]:18080",
|
||||||
|
} {
|
||||||
|
t.Run(value, func(t *testing.T) {
|
||||||
|
t.Setenv("PLATFORM_RUN_RELEASE_URL", value)
|
||||||
|
if _, err := runReleasePlatformURL(); err == nil || !strings.Contains(err.Error(), "reachable from the target server") {
|
||||||
|
t.Fatalf("expected target-local URL %q to be rejected, got %v", value, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunReleasePlatformURLAcceptsExternalHTTPURL(t *testing.T) {
|
||||||
|
t.Setenv("PLATFORM_RUN_RELEASE_URL", "http://platform.example.test:8080/control")
|
||||||
|
|
||||||
|
value, err := runReleasePlatformURL()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve external Run release URL: %v", err)
|
||||||
|
}
|
||||||
|
if value != "http://platform.example.test:8080/control" {
|
||||||
|
t.Fatalf("unexpected external Run release URL %q", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunReleasePlatformURLRejectsInvalidURLForms(t *testing.T) {
|
||||||
|
for _, value := range []string{
|
||||||
|
"ftp://platform.example.test",
|
||||||
|
"https://operator@platform.example.test",
|
||||||
|
"https://platform.example.test/?token=value",
|
||||||
|
"https://platform.example.test/?",
|
||||||
|
"https://platform.example.test/#fragment",
|
||||||
|
"https://platform.example.test/#",
|
||||||
|
} {
|
||||||
|
t.Run(value, func(t *testing.T) {
|
||||||
|
t.Setenv("PLATFORM_RUN_RELEASE_URL", value)
|
||||||
|
if _, err := runReleasePlatformURL(); err == nil || !strings.Contains(err.Error(), "absolute http or https URL") {
|
||||||
|
t.Fatalf("expected invalid Run release URL %q to be rejected, got %v", value, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceRejectsLoopbackRunReleaseURLBeforeGeneration(t *testing.T) {
|
||||||
|
t.Setenv("PLATFORM_RUN_RELEASE_URL", "http://127.0.0.1:18080")
|
||||||
|
svc, session, instance := newDistributionTestFixture(t)
|
||||||
|
|
||||||
|
_, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
||||||
|
ServerInstanceID: instance.ID,
|
||||||
|
TargetOS: "windows",
|
||||||
|
TargetArch: "amd64",
|
||||||
|
IdempotencyKey: "loopback-release-url",
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "reachable from the target server") {
|
||||||
|
t.Fatalf("expected loopback Run release URL to fail generation, got %v", err)
|
||||||
|
}
|
||||||
|
distributions, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil || len(distributions) != 0 {
|
||||||
|
t.Fatalf("invalid release URL must not create distributions, distributions=%+v err=%v", distributions, err)
|
||||||
|
}
|
||||||
|
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{ServerInstanceID: instance.ID, ComponentKind: domain.DistributionComponentRun})
|
||||||
|
if err != nil || len(keys) != 0 {
|
||||||
|
t.Fatalf("invalid release URL must not create component keys, keys=%+v err=%v", keys, err)
|
||||||
|
}
|
||||||
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil || len(jobs) != 0 {
|
||||||
|
t.Fatalf("invalid release URL must not create build jobs, jobs=%+v err=%v", jobs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
|
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
|
||||||
svc, session, instance := newDistributionTestFixture(t)
|
svc, session, instance := newDistributionTestFixture(t)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export PLATFORM_BOOTSTRAP_ADMIN_EMAIL="${PLATFORM_BOOTSTRAP_ADMIN_EMAIL:-operato
|
|||||||
export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}"
|
export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}"
|
||||||
export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}"
|
export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}"
|
||||||
export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}"
|
export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}"
|
||||||
export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-http://127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}"
|
export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-https://scum.npc0.com/}"
|
||||||
|
|
||||||
export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}"
|
export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}"
|
||||||
export RUN_REPO_DIR="$RUN_SOURCE_DIR"
|
export RUN_REPO_DIR="$RUN_SOURCE_DIR"
|
||||||
|
|||||||
@@ -1571,8 +1571,8 @@ if [[ "$VITE_PLATFORM_API_BASE_URL" != "/api/v1" ]]; then
|
|||||||
printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2
|
printf 'VITE_PLATFORM_API_BASE_URL must be /api/v1, got %s\n' "$VITE_PLATFORM_API_BASE_URL" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$PLATFORM_RUN_RELEASE_URL" != "$PLATFORM_URL" ]]; then
|
if [[ "$PLATFORM_RUN_RELEASE_URL" == "$PLATFORM_URL" ]]; then
|
||||||
printf 'PLATFORM_RUN_RELEASE_URL must be %s for executable local Run proof, got %s\n' "$PLATFORM_URL" "$PLATFORM_RUN_RELEASE_URL" >&2
|
printf 'PLATFORM_RUN_RELEASE_URL must use an address reachable from the generated Run target, not the local platform URL %s\n' "$PLATFORM_URL" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then
|
if [[ "$PLATFORM_API_PROXY" != "$PLATFORM_URL" ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user