Files
cameleer-server/cameleer-server-app/src/main/resources/application.yml
hsiegeln 1ddae94930 feat(runtime): init-container loader pattern + withUsernsMode (#152 hardening close)
Tasks 9+10+11 of the init-container-jar-fetch plan, landed atomically because
9 alone leaves the orchestrator+executor referencing removed ContainerRequest
fields.

ContainerRequest (core) drops jarPath/jarVolumeName/jarVolumeMountPath; adds
appVersionId, artifactDownloadUrl, artifactExpectedSize, loaderImage.

DockerRuntimeOrchestrator (app):
  - per-replica named volume "cameleer-jars-{containerName}"
  - phase 1: loader container with the volume mounted RW at /app/jars,
    ARTIFACT_URL + ARTIFACT_EXPECTED_SIZE env, full hardening contract
  - block on waitContainerCmd().awaitStatusCode(120s); on non-zero exit
    remove the loader, remove the volume, propagate RuntimeException so
    DeploymentExecutor marks the deployment FAILED. main is never created.
  - phase 2: main container with the same volume mounted RO at /app/jars
  - withUsernsMode("host:1000:65536") on BOTH containers — closes the last
    open hardening gap from issue #152
  - main entrypoint paths point at /app/jars/app.jar
  - extracted baseHardenedHostConfig() so loader and main share the
    cap_drop / security_opt / readonly / pids / tmpfs contract
  - removeContainer() also removes the per-replica volume so blue/green
    doesn't leak volumes

DeploymentExecutor (app):
  - injects ArtifactDownloadTokenSigner; new @Value props loaderimage,
    artifacttokenttlseconds, artifactbaseurl
  - replaces the temporary getVersion(...).jarPath() bridge with a signed
    URL ${artifactBaseUrl}/api/v1/artifacts/{id}?exp&sig
  - drops the Files.exists pre-flight check; AppVersion.jarSizeBytes is
    the size-of-record check now
  - drops jarDockerVolume / jarStoragePath @Value fields and the volume
    plumbing in startReplica
  - DeployCtx carries appVersionId / artifactUrl / artifactExpectedSize
    in place of jarPath

Tests:
  - DockerRuntimeOrchestratorHardeningTest updated for the new shape;
    captures HostConfig on the MAIN container and asserts cap_drop ALL
    + no-new-privileges + apparmor + readonly + pids + tmpfs + the new
    withUsernsMode("host:1000:65536")
  - DockerRuntimeOrchestratorLoaderTest (new): verifies volume create →
    loader create with RW bind → loader started → awaited → loader
    removed → main create with RO bind → main started; verifies abort
    + cleanup on loader exit != 0 (loader removed, volume removed, main
    NEVER created); verifies userns_mode applied to both containers.

Config:
  - application.yml replaces jardockervolume with loaderimage,
    artifacttokenttlseconds, artifactbaseurl

Rules updated: .claude/rules/docker-orchestration.md (loader pattern,
userns, no more bind-mount); .claude/rules/core-classes.md
(ContainerRequest field map).

Test counts after change:
  - cameleer-server-core: 116/116 unit tests pass
  - cameleer-server-app: 273/273 unit tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:06:56 +02:00

151 lines
6.6 KiB
YAML

server:
port: 8081
spring:
servlet:
multipart:
max-file-size: 200MB
max-request-size: 200MB
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer?currentSchema=tenant_${cameleer.server.tenant.id}&ApplicationName=tenant_${cameleer.server.tenant.id}}
username: ${SPRING_DATASOURCE_USERNAME:cameleer}
password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev}
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
locations: classpath:db/migration
create-schemas: true
mvc:
async:
request-timeout: -1
mustache:
check-template-location: false
jackson:
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
cameleer:
server:
tenant:
id: ${CAMELEER_SERVER_TENANT_ID:default}
agentregistry:
heartbeatintervalms: 30000
stalethresholdms: 90000
deadthresholdms: 300000
pingintervalms: 15000
commandexpiryms: 60000
lifecyclecheckintervalms: 10000
ingestion:
buffercapacity: 50000
batchsize: 5000
flushintervalms: 5000
bodysizelimit: ${CAMELEER_SERVER_INGESTION_BODYSIZELIMIT:16384}
runtime:
enabled: ${CAMELEER_SERVER_RUNTIME_ENABLED:true}
jarstoragepath: ${CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH:/data/jars}
baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest}
dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer}
# Container runtime override. Empty (default) auto-detects: uses runsc
# (gVisor) if the daemon has it registered, otherwise the daemon default
# (runc). Set to a registered runtime name (e.g. "kata", "runc") to
# force a specific runtime. See issue #152 for the threat model.
dockerruntime: ${CAMELEER_SERVER_RUNTIME_DOCKERRUNTIME:}
agenthealthport: 9464
healthchecktimeout: 60
container:
memorylimit: ${CAMELEER_SERVER_RUNTIME_CONTAINER_MEMORYLIMIT:512m}
cpushares: ${CAMELEER_SERVER_RUNTIME_CONTAINER_CPUSHARES:512}
routingmode: ${CAMELEER_SERVER_RUNTIME_ROUTINGMODE:path}
routingdomain: ${CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN:localhost}
serverurl: ${CAMELEER_SERVER_RUNTIME_SERVERURL:}
certresolver: ${CAMELEER_SERVER_RUNTIME_CERTRESOLVER:}
# Init-container loader for tenant JAR fetch. The loader runs as a
# short-lived sidecar that downloads the JAR from a signed URL into a
# per-replica named volume, which the main container then mounts RO at
# /app/jars. See issue #152 close-out + .claude/rules/docker-orchestration.md.
loaderimage: ${CAMELEER_SERVER_RUNTIME_LOADERIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest}
artifacttokenttlseconds: ${CAMELEER_SERVER_RUNTIME_ARTIFACTTOKENTTLSECONDS:600}
artifactbaseurl: ${CAMELEER_SERVER_RUNTIME_ARTIFACTBASEURL:}
indexer:
debouncems: ${CAMELEER_SERVER_INDEXER_DEBOUNCEMS:2000}
queuesize: ${CAMELEER_SERVER_INDEXER_QUEUESIZE:10000}
catalog:
discoveryttldays: ${CAMELEER_SERVER_CATALOG_DISCOVERYTTLDAYS:7}
license:
token: ${CAMELEER_SERVER_LICENSE_TOKEN:}
file: ${CAMELEER_SERVER_LICENSE_FILE:}
publickey: ${CAMELEER_SERVER_LICENSE_PUBLICKEY:}
security:
accesstokenexpiryms: 3600000
refreshtokenexpiryms: 604800000
bootstraptoken: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN:}
bootstraptokenprevious: ${CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKENPREVIOUS:}
uiuser: ${CAMELEER_SERVER_SECURITY_UIUSER:admin}
uipassword: ${CAMELEER_SERVER_SECURITY_UIPASSWORD:admin}
uiorigin: ${CAMELEER_SERVER_SECURITY_UIORIGIN:http://localhost:5173}
jwtsecret: ${CAMELEER_SERVER_SECURITY_JWTSECRET:}
corsallowedorigins: ${CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS:}
infrastructureendpoints: ${CAMELEER_SERVER_SECURITY_INFRASTRUCTUREENDPOINTS:true}
oidc:
issueruri: ${CAMELEER_SERVER_SECURITY_OIDC_ISSUERURI:}
jwkseturi: ${CAMELEER_SERVER_SECURITY_OIDC_JWKSETURI:}
audience: ${CAMELEER_SERVER_SECURITY_OIDC_AUDIENCE:}
tlsskipverify: ${CAMELEER_SERVER_SECURITY_OIDC_TLSSKIPVERIFY:false}
alerting:
evaluator-tick-interval-ms: ${CAMELEER_SERVER_ALERTING_EVALUATORTICKINTERNALMS:5000}
evaluator-batch-size: ${CAMELEER_SERVER_ALERTING_EVALUATORBATCHSIZE:20}
claim-ttl-seconds: ${CAMELEER_SERVER_ALERTING_CLAIMTTLSECONDS:30}
notification-tick-interval-ms: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONTICKINTERNALMS:5000}
notification-batch-size: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONBATCHSIZE:50}
in-tick-cache-enabled: ${CAMELEER_SERVER_ALERTING_INTICKCACHEENABLED:true}
circuit-breaker-fail-threshold: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERFAILTHRESHOLD:5}
circuit-breaker-window-seconds: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERWINDOWSECONDS:30}
circuit-breaker-cooldown-seconds: ${CAMELEER_SERVER_ALERTING_CIRCUITBREAKERCOOLDOWNSECONDS:60}
event-retention-days: ${CAMELEER_SERVER_ALERTING_EVENTRETENTIONDAYS:90}
notification-retention-days: ${CAMELEER_SERVER_ALERTING_NOTIFICATIONRETENTIONDAYS:30}
webhook-timeout-ms: ${CAMELEER_SERVER_ALERTING_WEBHOOKTIMEOUTMS:5000}
webhook-max-attempts: ${CAMELEER_SERVER_ALERTING_WEBHOOKMAXATTEMPTS:3}
# PER_EXCHANGE first-run cursor clamp: on first tick with no persisted cursor, evaluator
# scans no further back than (now - this cap). Prevents one-time backlog flood for rules
# whose createdAt predates a migration. Set to 0 to disable and replay from createdAt.
per-exchange-deploy-backlog-cap-seconds: ${CAMELEER_SERVER_ALERTING_PEREXCHANGEDEPLOYBACKLOGCAPSECONDS:86400}
outbound-http:
trust-all: false
trusted-ca-pem-paths: []
default-connect-timeout-ms: 2000
default-read-timeout-ms: 5000
# proxy-url:
# proxy-username:
# proxy-password:
clickhouse:
url: ${CAMELEER_SERVER_CLICKHOUSE_URL:jdbc:clickhouse://localhost:8123/cameleer}
username: ${CAMELEER_SERVER_CLICKHOUSE_USERNAME:default}
password: ${CAMELEER_SERVER_CLICKHOUSE_PASSWORD:}
self-metrics:
enabled: ${CAMELEER_SERVER_SELFMETRICS_ENABLED:true}
interval-ms: ${CAMELEER_SERVER_SELFMETRICS_INTERVALMS:60000}
instance-id: ${CAMELEER_SERVER_INSTANCE_ID:}
springdoc:
api-docs:
path: /api/v1/api-docs
swagger-ui:
path: /api/v1/swagger-ui
logging:
level:
com.clickhouse: INFO
org.apache.hc.client5: WARN
management:
endpoints:
web:
base-path: /api/v1
exposure:
include: health,prometheus
endpoint:
health:
show-details: always