Compare commits

..
28 Commits
Author SHA1 Message Date
boxi e29ca86d84 added the full version to VERSION in config.tar.gz
Build Remsly / Compute-Version (push) Successful in 4s
Build Remsly / Build-Remsly-Backend (push) Successful in 2m12s
Build Remsly / Build-Remsly-Frontend (push) Successful in 1m23s
Build Remsly / Package-Prod-Config (push) Successful in 5s
2026-07-13 23:04:14 +02:00
boxi db896be056 fixed FE error on load Map-tiles 2026-07-13 22:47:34 +02:00
boxi 4e6d7f7b02 fixed FE handling when no coordinates were found
Build Remsly / Compute-Version (push) Successful in 4s
Build Remsly / Build-Remsly-Backend (push) Successful in 2m11s
Build Remsly / Build-Remsly-Frontend (push) Successful in 1m30s
Build Remsly / Package-Prod-Config (push) Successful in 6s
2026-07-13 22:04:06 +02:00
boxi 311f3c9818 fixed nominatim api call in BE 2026-07-13 21:28:37 +02:00
boxi 4269f980db cleanUp Intellij warnings
Build Remsly / Compute-Version (push) Successful in 5s
Build Remsly / Build-Remsly-Backend (push) Successful in 2m21s
Build Remsly / Build-Remsly-Frontend (push) Successful in 1m28s
Build Remsly / Package-Prod-Config (push) Successful in 5s
2026-07-12 20:39:55 +02:00
boxi c23836e5fe init deploy.sh 2026-07-12 20:32:47 +02:00
boxi 7ccf9e1bc3 init versioning JJJJ.MM-<MAYOR>-<BuildNR> & config upload 2026-07-12 16:56:44 +02:00
boxi 8c6e31166a fixed ES migrationFile loading 2026-07-12 14:49:40 +02:00
boxi 0e2120e414 copied config from prod 2026-07-12 13:35:10 +02:00
boxi d09fb5d9b7 fixed some FR warnings & bugs 2026-07-12 13:11:49 +02:00
boxi 99d683796a remove unused truststore 2026-07-12 12:44:06 +02:00
boxi a6fb0e41fe fixinf deeplinks 2026-07-09 23:22:03 +02:00
boxi ebb59daad4 update ProdBuild try wireing docker and tailscale
Build Remsly / Build-Remsly-Backend (push) Successful in 2m15s
Build Remsly / Build-Remsly-Frontend (push) Successful in 1m23s
2026-07-09 00:09:17 +02:00
boxi f02f4a295c update ProdBuild 2026-07-08 20:47:58 +02:00
boxi 6a50099849 update ProdBuild
Build Remsly / Build-Remsly-Backend (push) Successful in 2m36s
Build Remsly / Build-Remsly-Frontend (push) Successful in 1m28s
2026-07-06 19:27:13 +02:00
boxi 5c92129fba use new docker images
Build Remsly / Build-Remsly-Backend (push) Successful in 2m20s
2026-06-29 22:52:17 +02:00
boxi 7807494b34 Force Build 2026-06-29 22:27:26 +02:00
boxi e5edf055ea fix docker image
Build Remsly / Build-Remsly-Backend (push) Successful in 2m8s
2026-06-28 20:40:13 +02:00
boxi a0960feeb3 adjust for new GiteaServer
Build Remsly / Build-Remsly-Backend (push) Failing after 7s
2026-06-28 20:37:14 +02:00
boxi 34156807e4 use new ApacheOIDC Image for localDev deployment 2026-01-06 22:29:25 +01:00
boxi 3c655a5dba rename docker image 2026-01-02 11:59:43 +01:00
boxi 974d5df8b4 added imageTag output 2026-01-02 11:11:21 +01:00
boxi ab24be8f3c fix run use V2 2026-01-02 10:38:40 +01:00
boxi 8104c959be fix run use 2026-01-02 10:37:12 +01:00
boxi 2151f6661f rename pipeline 2026-01-02 10:35:38 +01:00
boxi c2c56f7c87 build docker image during CI 2026-01-02 10:31:46 +01:00
boxi 04be2b4714 use temurin java 2026-01-02 10:31:26 +01:00
boxi 243edd4355 use ci-image 2026-01-01 22:46:32 +01:00
207 changed files with 891 additions and 649 deletions

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
name: Build Remsly
on:
push:
branches: [ main ]
jobs:
Compute-Version:
runs-on: ubuntu-latest
container:
image: gitea.tail276105.ts.net/remsly/ci-image:5
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Compute version
id: v
run: |
MAJOR=$(tr -d '[:space:]' < deploy/prod/VERSION)
VERSION="$(date -u +%Y.%m)-${MAJOR}-${GITHUB_RUN_NUMBER}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Computed version: $VERSION"
Build-Remsly-Backend:
runs-on: ubuntu-latest
needs: Compute-Version
container:
image: gitea.tail276105.ts.net/remsly/ci-image:5
env:
VERSION: ${{ needs.Compute-Version.outputs.version }}
DB_HOST_PORT: buildPostgres:5432
DB_USER: develop
DB_PASSWD: develop
DB_NAME: develop
services:
buildPostgres:
image: postgres:14-alpine
env:
POSTGRES_USER: develop
POSTGRES_PASSWORD: develop
POSTGRES_DB: develop
options: >-
--health-cmd="pg_isready -U develop -d develop -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=12
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build BE
run: ./gradlew build -PDB_HOST_PORT=buildPostgres:5432 -PDB_USER=develop -PDB_PASSWD=develop -PDB_NAME=develop;
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: gitea.tail276105.ts.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
(
cd backend;
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION} .;
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION} gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest
)
- name: Push Docker Images
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION}
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest
- name: Docker imageTags
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
echo "versioned: gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:${VERSION}"
echo "latest: gitea.tail276105.ts.net/${REPOSITORY_LC}-backend:latest"
- name: Log out from Docker registry
if: always()
run: docker logout gitea.tail276105.ts.net
Build-Remsly-Frontend:
runs-on: ubuntu-latest
needs: Compute-Version
container:
image: gitea.tail276105.ts.net/remsly/ci-image:5
env:
VERSION: ${{ needs.Compute-Version.outputs.version }}
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build FE
run: |
cd frontend
npm ci
npm run buildProd
- name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: gitea.tail276105.ts.net
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
(
cd frontend;
docker build -t gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION} .;
docker tag gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION} gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest
)
- name: Push Docker Images
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION}
docker push gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest
- name: Docker imageTags
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
echo "versioned: gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:${VERSION}"
echo "latest: gitea.tail276105.ts.net/${REPOSITORY_LC}-frontend:latest"
- name: Log out from Docker registry
if: always()
run: docker logout gitea.tail276105.ts.net
Package-Prod-Config:
runs-on: ubuntu-latest
needs: [ Compute-Version, Build-Remsly-Backend, Build-Remsly-Frontend ]
container:
image: gitea.tail276105.ts.net/remsly/ci-image:5
env:
VERSION: ${{ needs.Compute-Version.outputs.version }}
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Bake image versions into docker-compose.yml
run: |
# The compose file pins backend/frontend to the <<VERSION>> placeholder;
# replace it with this build's version. The manually maintained services
# (elasticsearch, keycloak, postgres, tailscale) keep their own tags.
sed -i "s|<<VERSION>>|${VERSION}|g" deploy/prod/docker-compose.yml
echo "Pinned backend/frontend images to ${VERSION}:"
grep -nE "remsly-(backend|frontend)" deploy/prod/docker-compose.yml
- name: Write full version into VERSION file
run: |
printf '%s\n' "${VERSION}" > deploy/prod/VERSION
echo "config.tar.gz VERSION -> ${VERSION}"
- name: Build config.tar.gz
run: tar -czf config.tar.gz -C deploy/prod .
- name: Upload config to Gitea generic package registry
run: |
REPOSITORY_LC=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
OWNER=${REPOSITORY_LC%%/*}
curl -fsSL --user "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_PASSWORD }}" \
--upload-file config.tar.gz \
"https://gitea.tail276105.ts.net/api/packages/${OWNER}/generic/prod-config/${VERSION}/config.tar.gz"
echo "Uploaded prod-config/${VERSION}/config.tar.gz"
-44
View File
@@ -1,44 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
container:
image: eclipse-temurin:17
env:
DB_HOST_PORT: buildPostgres:5432
DB_USER: develop
DB_PASSWD: develop
DB_NAME: develop
services:
buildPostgres:
image: postgres:14-alpine
env:
POSTGRES_USER: develop
POSTGRES_PASSWORD: develop
POSTGRES_DB: develop
options: >-
--health-cmd="pg_isready -U develop -d develop -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=12
steps:
- run : uname -n
- run: echo $PATH
- run: printenv
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: Check out repository code
uses: actions/checkout@v4
- name: Build BE
run: ./gradlew build -PDB_HOST_PORT=buildPostgres:5432 -PDB_USER=develop -PDB_PASSWD=develop -PDB_NAME=develop;
- run: cp backend/build/libs/backend-version.jar /mnt/space/share/backend-version.jar;
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
+1
View File
@@ -5,6 +5,7 @@
/frontend/build/
!gradle/wrapper/gradle-wrapper.jar
http-client.private.env.json
deploy/prod/.env
/backend/src/main/resources/*.properties
src/gen
+80
View File
@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Boxit (also called "Remsly" in CI) is a box/inventory management app:
- **backend/** — Spring Boot 2.6 (Kotlin, Java 17, Jersey + Spring MVC), PostgreSQL via jOOQ, Elasticsearch for search, Keycloak for auth. Runs on port **5050**.
- **frontend/** — Angular 13 app (port **4200**), proxies `/api` to the backend via `proxy-conf.json`.
- **JooqHelper/** — small Gradle module providing `BoxitNamingStrategy` used by jOOQ code generation.
- **deploy/localDev/boxit/** — docker-compose for local infrastructure (Postgres on 6543, Keycloak on 8081, Apache OIDC proxy, Elasticsearch, Prometheus).
- **deploy/prod/** — production docker-compose + Let's Encrypt scripts.
- **clients/** — IntelliJ HTTP client files for the REST API, Keycloak, and Elasticsearch (env in `http-client.private.env.json`, template provided).
## Commands
### Backend (Gradle, from repo root)
```bash
./gradlew build # build + tests (requires running Postgres, see below)
./gradlew test # tests only (JUnit 5; integration tests use Testcontainers)
./gradlew generateJooq # run Flyway migrations, then regenerate jOOQ classes
./gradlew lintKotlin # kotlinter lint
./gradlew formatKotlin # kotlinter autoformat
./gradlew bootRun # run the backend
```
Run a single test class: `./gradlew test --tests "ch.boxi.boxit.SomeTestClass"`
DB connection for Flyway/jOOQ codegen is parameterized via Gradle properties
`-PDB_HOST_PORT -PDB_USER -PDB_PASSWD -PDB_NAME` (defaults: `localhost:6543`, `gradle`/`gradlepw`/`gradle` — matches the localDev docker-compose Postgres).
### Frontend (from frontend/)
```bash
npm start # dev server on :4200 with /api proxy to :5050
npm run build # dev build
npm run buildProd # production build (also copies index.html -> 404.html)
npm test # Karma/Jasmine tests
npm run lint # ng lint
```
### Local infrastructure
```bash
cd deploy/localDev/boxit && docker compose up -d
```
Provides Postgres (port 6543, multiple DBs: `boxit`, `keycloak`, `gradle`, `test`), Keycloak (8081), Apache OIDC proxy, and Elasticsearch. `backend/src/main/resources/application.properties` is not committed — create it from `application.properties-Template`.
## Architecture
### Backend data flow: Flyway → jOOQ codegen
The DB schema is the source of truth. Migrations live in `backend/src/main/resources/db/migration` (Flyway, `V*.sql`). `generateJooq` depends on `flywayMigrate` and generates records/POJOs/DAOs into `backend/src/gen/java` (package `ch.boxi.boxit.gen`, deleted on `clean`, regenerated on compilation). Naming is customized by `JooqHelper`'s `BoxitNamingStrategy`. **After changing a migration, re-run `./gradlew generateJooq` against a running Postgres.**
### Backend layers (`ch.boxi.boxit`)
- `restapi/` — REST controllers, DTOs, mappers, pagination (`Paged`, `PaginationDto`, infinite scroll support). Organized by feature (folder, note, task, user, search).
- `domain/` — business logic per feature (attachment, folder, location, note, task, user). Uses a Visitor pattern (`Visitable`/`Visitor`) and `Augmentor` abstractions.
- `db/` — jOOQ-based repositories built on `SimpleCrudRepository` / `AbstractSimpleCrudRepositoryImpl` plus composable `JooqFilter` classes (`CombinedAndFilter`, `CombinedOrFilter`, ...).
- `searchindex/` — Elasticsearch index per feature (folder, note, task) with its own migration support; `db/elasticsearchSync` + `domain/elasticSearchSync` implement DB→ES synchronization.
- `clientapi/` — outbound HTTP clients (e.g. OpenStreetMap geocoding).
- `security/` — Keycloak/OIDC config and SSL client config (`SSLClientConfig` pins outbound HTTPS to TLS 1.2; trust uses the JVM default cacerts — no custom truststore).
### Frontend layers (`frontend/src/app`)
- `api/` — `boxit.api.ts` + DTOs + HTTP error interceptor (mirror of the backend REST API).
- `business/` — services/models per feature (item, attachment, location, user), notification and global error handling.
- `gui/` — components (header, page, search, filters, item, location, responsive helpers).
- `auth/` — `auth.service.ts` (Keycloak-based login).
### Testing
Backend integration tests (`BoxitIntegrationTest`) use Testcontainers. For container reuse, add `testcontainers.reuse.enabled=true` to `~/.testcontainers.properties`.
### CI
Gitea Actions (`.gitea/workflows/`) builds on push to `main`: Gradle build with a Postgres service container, then builds/pushes Docker images (`*-backend`, `*-frontend`) to `gitea.tail276105.ts.net` and triggers a prod compose redeploy. `bitbucket-pipelines.yml` and `.space.kts` are legacy.
+1 -1
View File
@@ -1,3 +1,3 @@
FROM amazoncorretto:17
FROM eclipse-temurin:17
ADD build/libs/backend-version.jar boxit-backend.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom", "-Xmx1024m","-jar","/boxit-backend.jar"]
+1 -1
View File
@@ -33,7 +33,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.keycloak:keycloak-spring-boot-starter:16.0.0")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.micrometer:micrometer-core")
implementation("io.micrometer:micrometer-registry-prometheus")
@@ -14,7 +14,7 @@ import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
abstract class AbstractRestApi protected constructor(private val metrics: OutGoingRestMetrics, private val externalSystemName: String, private val baseUrl: String) : AbstractRequestResponseLogger(AbstractRequestResponseLogger.Channel.OUTGOING) {
abstract class AbstractRestApi protected constructor(private val metrics: OutGoingRestMetrics, private val externalSystemName: String, private val baseUrl: String, private val additionalHeaders: Map<String, String> = emptyMap()) : AbstractRequestResponseLogger(AbstractRequestResponseLogger.Channel.OUTGOING) {
protected fun httpGet(uri: String, vararg parameters: Parameter): String {
return call(
@@ -34,18 +34,17 @@ abstract class AbstractRestApi protected constructor(private val metrics: OutGoi
val con = url.openConnection() as HttpURLConnection
con.requestMethod = method.name
con.setRequestProperty(REQUEST_PROPERTY_ACCEPT, APPLICATION_JSON)
additionalHeaders.forEach { (key, value) -> con.setRequestProperty(key, value) }
if (body != null) {
val writer = OutputStreamWriter(con.outputStream)
writer.write(body)
writer.flush()
}
status = HttpStatus.valueOf(con.responseCode)
val reader = BufferedReader(InputStreamReader(con.inputStream))
val sb = StringBuilder()
while (reader.ready()) {
sb.append(reader.readLine())
}
body = sb.toString()
// Read the whole response to EOF. Do NOT loop on reader.ready(): for a network
// stream the body may not be buffered yet right after responseCode, so ready()
// can return false immediately and yield an empty/truncated body.
body = BufferedReader(InputStreamReader(con.inputStream, Charsets.UTF_8)).use { it.readText() }
handleResponse(method, url, uri, stopwatch, con, body)
return body
} catch (e: IOException) {
@@ -10,13 +10,17 @@ import ch.boxi.boxit.infrastructur.monitoring.http.OutGoingRestMetrics
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
@Service
class NominatimApi @Autowired
constructor(metrics: OutGoingRestMetrics) : AbstractRestApi(metrics, OSM_NOMINATIM, BASE_URL) {
constructor(
metrics: OutGoingRestMetrics,
@Value("\${boxit.nominatim.contact-email}") contactEmail: String,
) : AbstractRestApi(metrics, OSM_NOMINATIM, BASE_URL, mapOf(HEADER_USER_AGENT to userAgent(contactEmail))) {
@Throws(BoxitRuntimeException::class)
fun resolveLocation(location: String?): Coordinates? {
@@ -77,8 +81,11 @@ constructor(metrics: OutGoingRestMetrics) : AbstractRestApi(metrics, OSM_NOMINAT
}
companion object : Logger {
private val BASE_URL = "https://nominatim.openstreetmap.org"
val OSM_NOMINATIM = "OSM-Nominatim"
val SEARCH_URI = "/search?format=json&q={location}"
private const val BASE_URL = "https://nominatim.openstreetmap.org"
private const val OSM_NOMINATIM = "OSM-Nominatim"
private const val SEARCH_URI = "/search?format=jsonv2&q={location}"
private const val HEADER_USER_AGENT = "User-Agent"
private fun userAgent(contactEmail: String) = "Remsly/0.1 ($contactEmail)"
}
}
@@ -2,8 +2,8 @@ package ch.boxi.boxit.domain.user
import ch.boxi.boxit.db.user.UserRepository
import ch.boxi.boxit.exception.BoxitRuntimeException
import org.keycloak.KeycloakPrincipal
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.stereotype.Service
import java.io.InputStream
import java.time.Instant
@@ -14,11 +14,9 @@ class UserService(private val userRepository: UserRepository) {
val currentUserOptional: User?
get() {
val securityContext = SecurityContextHolder.getContext()
val authentication = securityContext.authentication
return if (authentication != null) {
val keycloakPrincipal = authentication.principal as KeycloakPrincipal<*>
val extId = AuthenticationExtId(keycloakPrincipal.name)
val authentication = SecurityContextHolder.getContext().authentication
return if (authentication is JwtAuthenticationToken) {
val extId = AuthenticationExtId(authentication.token.subject)
findBy(extId)
} else {
null
@@ -7,14 +7,11 @@ import ch.boxi.boxit.searchindex.migration.data.MigrationOperation
import ch.boxi.boxit.searchindex.migration.data.MigrationScript
import ch.boxi.boxit.searchindex.migration.data.Version
import com.google.common.base.Strings
import com.google.common.io.PatternFilenameFilter
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.http.HttpMethod
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import java.nio.file.Path
import java.util.Arrays
import java.util.SortedSet
import java.util.regex.Pattern
@@ -29,35 +26,35 @@ class MigrationLoader {
}
fun loadMigrations(): SortedSet<MigrationScript> {
val filenameFilter = PatternFilenameFilter(FILENAME_PATTERN)
val folderResource = ClassPathResource("elasticsearch")
return Arrays.stream(folderResource.file.listFiles(filenameFilter))
.map(File::toPath)
val resources = PathMatchingResourcePatternResolver().getResources("classpath*:elasticsearch/*.edql")
return Arrays.stream(resources)
.filter { resource -> resource.filename?.let { FILENAME_PATTERN.matcher(it).matches() } ?: false }
.map(this::loadMigration)
.toSortedSet()
}
private fun loadMigration(path: Path): MigrationScript {
LOG.debug("found migration file $path")
val matcher = FILENAME_PATTERN.matcher(path.fileName.toString())
private fun loadMigration(resource: Resource): MigrationScript {
val filename = resource.filename!!
LOG.debug("found migration file $filename")
val matcher = FILENAME_PATTERN.matcher(filename)
matcher.matches()
val script = matcher.group(0)
val version = Version.parse(matcher.group(1))
val description = matcher.group(2)
val operations = readOperations(path)
val operations = readOperations(resource)
return MigrationScript(version, description, script, operations.second.value, operations.first)
}
private fun readOperations(path: Path): Pair<List<MigrationOperation>, Checksum> {
private fun readOperations(resource: Resource): Pair<List<MigrationOperation>, Checksum> {
val operations = mutableListOf<MigrationOperation>()
var httpMethod: HttpMethod? = null
var operationPath: String? = null
val checkSum: Checksum
var body = StringBuilder()
CheckedInputStream(FileInputStream(path.toFile()), CRC32()).use { checkedReader ->
CheckedInputStream(resource.inputStream, CRC32()).use { checkedReader ->
BufferedReader(InputStreamReader(checkedReader)).use { reader ->
reader.lines().forEach { line ->
val subLine = line.trim()
@@ -1,68 +0,0 @@
package ch.boxi.boxit.security
import ch.boxi.boxit.domain.user.AuthenticationExtId
import ch.boxi.boxit.domain.user.UserService
import ch.boxi.boxit.infrastructur.logging.Logger
import ch.boxi.boxit.infrastructur.logging.LoggerHelper
import org.keycloak.KeycloakPrincipal
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.FilterType
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import javax.mail.internet.InternetAddress
// @KeycloakConfiguration
@Configuration
@ComponentScan(
basePackages = ["org.keycloak.adapters.springsecurity"],
excludeFilters = [ComponentScan.Filter(type = FilterType.REGEX, pattern = arrayOf("org.keycloak.adapters.springsecurity.management.HttpSessionManager"))]
)
@EnableWebSecurity
class KeycloakConfig
@Autowired
constructor(private val userService: UserService) : KeycloakWebSecurityConfigurerAdapter() {
companion object : Logger
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return SessionAuthenticationStrategy { authentication, _, _ ->
if (authentication is KeycloakAuthenticationToken) {
val principal = authentication.principal
if (principal is KeycloakPrincipal<*>) {
val email = principal.keycloakSecurityContext.token.email
val extId = AuthenticationExtId(principal.name)
val username = principal.keycloakSecurityContext.token.preferredUsername
LoggerHelper.user = username
userService.getCreateOrUpdate(extId, username, InternetAddress(email))
}
}
}
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
super.configure(http)
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers()
.and()
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.anyRequest().authenticated()
}
@Bean
fun authenticationProvider(): KeycloakAuthenticationProvider {
return KeycloakAuthenticationProvider()
}
}
@@ -1,7 +0,0 @@
package ch.boxi.boxit.security
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.springframework.context.annotation.Configuration
@Configuration
class KeycloakConfigResolver : KeycloakSpringBootConfigResolver()
@@ -1,21 +1,16 @@
package ch.boxi.boxit.security
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
import javax.annotation.PostConstruct
@Configuration
class SSLClientConfig {
@Autowired
private val env: Environment? = null
@PostConstruct
private fun configureSSL() {
// Pin outbound HttpsURLConnection to TLS 1.2. Trust uses the JVM's default cacerts (no custom
// trust store): all internal services run over plain HTTP, and the only outbound HTTPS targets
// are public hosts (Gravatar, OSM) that the built-in CA bundle already trusts.
System.setProperty("https.protocols", "TLSv1.2")
val trustStore = env!!.getProperty("boxit.server.ssl.trust-store")
System.setProperty("javax.net.ssl.trustStore", trustStore)
System.setProperty("javax.net.ssl.trustStorePassword", env.getProperty("boxit.server.ssl.trust-store-password")!!)
}
}
@@ -0,0 +1,35 @@
package ch.boxi.boxit.security
import ch.boxi.boxit.domain.user.UserService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
import org.springframework.security.web.SecurityFilterChain
/**
* Stateless bearer-only security: every request must carry a Keycloak-issued JWT (forwarded by the
* Apache mod_auth_openidc proxy as `Authorization: Bearer ...`). The token is validated against the
* signing keys from `spring.security.oauth2.resourceserver.jwt.jwk-set-uri` and its issuer against
* `...jwt.issuer-uri` (see application.properties). Replaces the retired Keycloak Spring Boot adapter.
*/
@Configuration
@EnableWebSecurity
class SecurityConfig(private val userService: UserService) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.csrf { it.disable() }
.anonymous { it.disable() }
.authorizeRequests { it.anyRequest().authenticated() }
.oauth2ResourceServer { it.jwt { } }
// Provision/refresh the local user record from the validated token on every request,
// mirroring the behaviour of the previous Keycloak SessionAuthenticationStrategy.
.addFilterAfter(UserProvisioningFilter(userService), BearerTokenAuthenticationFilter::class.java)
return http.build()
}
}
@@ -0,0 +1,35 @@
package ch.boxi.boxit.security
import ch.boxi.boxit.domain.user.AuthenticationExtId
import ch.boxi.boxit.domain.user.UserService
import ch.boxi.boxit.infrastructur.logging.LoggerHelper
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.filter.OncePerRequestFilter
import javax.mail.internet.InternetAddress
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* Runs after the bearer-token authentication filter: for every authenticated request it upserts the
* local user record from the validated JWT (subject -> extId, `preferred_username`, `email`) and
* stamps the username onto the log context. Not a Spring-managed bean on purpose, so it is only
* active inside the security filter chain (where the SecurityContext is already populated).
*/
class UserProvisioningFilter(private val userService: UserService) : OncePerRequestFilter() {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val authentication = SecurityContextHolder.getContext().authentication
if (authentication is JwtAuthenticationToken) {
val jwt = authentication.token
val username = jwt.getClaimAsString("preferred_username") ?: jwt.subject
LoggerHelper.user = username
val email = jwt.getClaimAsString("email")
if (email != null) {
userService.getCreateOrUpdate(AuthenticationExtId(jwt.subject), username, InternetAddress(email))
}
}
filterChain.doFilter(request, response)
}
}
@@ -11,10 +11,9 @@ boxit.elasticsearch.port=9200
boxit.elasticsearch.user=elastic
boxit.elasticsearch.pass=elasticPassword
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
boxit.nominatim.contact-email=your.name@example.com
boxit.server.ssl.trust-store=src/main/resources/truststore.jks
boxit.server.ssl.trust-store-password=Tester11
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
@@ -23,10 +22,9 @@ management.metrics.export.prometheus.enabled=true
logging.level.root=INFO
keycloak.realm=boxit
keycloak.bearer-only=true
#keycloak.auth-server-url=https://id.boxi.ch/auth
keycloak.auth-server-url=http://kubernetes.docker.internal:8081
#keycloak.ssl-required=external
keycloak.resource=localdev-server
# Bearer-only JWT validation via Spring Security OAuth2 resource server.
# issuer-uri must match the "iss" claim in the tokens your Keycloak issues (adjust host/realm).
# jwk-set-uri is where the signing keys are fetched from - reachable from the backend.
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://kubernetes.docker.internal:8081/realms/boxit
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://kubernetes.docker.internal:8081/realms/boxit/protocol/openid-connect/certs
Binary file not shown.
@@ -10,11 +10,9 @@ boxit.elasticsearch.port=9200
boxit.elasticsearch.user=elastic
boxit.elasticsearch.pass=elasticPassword
spring.jackson.date-format="yyyy.MM.dd HH:mm:ss"
boxit.nominatim.contact-email=test@example.com
#boxit.server.ssl.trust-store=truststore
boxit.server.ssl.trust-store=src/main/resources/truststore.jks
boxit.server.ssl.trust-store-password=Tester11
spring.jackson.date-format="yyyy.MM.dd HH:mm:ss"
management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
@@ -23,9 +21,8 @@ management.metrics.export.prometheus.enabled=true
logging.level.root=INFO
keycloak.realm=boxit
keycloak.bearer-only=true
keycloak.auth-server-url=https://id.boxi.ch/auth
keycloak.ssl-required=external
keycloak.resource=localdev-server
keycloak.confidential-port=0
# Bearer-only JWT validation via Spring Security OAuth2 resource server. The decoder is lazy
# (no network call at context startup), so these dummy URLs are enough for the context to load;
# the tests exercise the service layer directly and never traverse the security filter chain.
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/realms/boxit
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8081/realms/boxit/protocol/openid-connect/certs
@@ -147,7 +147,7 @@ IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
# Include list of ports to listen on
Include ports.conf
# Include ports.conf
# Sets the default security model of the Apache2 HTTPD server. It does
@@ -237,7 +237,7 @@ LimitRequestFieldSize 32760
ServerName ${SERVER_NAME}
#Listen 80
Listen 80
<VirtualHost *:80>
DocumentRoot "/var/www/html/app"
LogLevel ${APACHE_DEBUG_LEVEL}
@@ -251,7 +251,7 @@ ServerName ${SERVER_NAME}
form-action 'self'; \
frame-ancestors 'none'; \
img-src 'self' api.mapbox.com data:; \
#upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
Header always append X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy "same-origin"
@@ -286,7 +286,6 @@ ServerName ${SERVER_NAME}
OIDCRedirectURI ${OIDC_REDIRECT_URI}
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
OIDCInfoHook ${OIDC_INFO_HOOK}
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2Fkubernetes.docker.internal%3A8800
# maps the prefered_username claim to the REMOTE_USER environment variable
+3 -3
View File
@@ -32,7 +32,7 @@ services:
# # http://localhost:8081/auth/realms/boxit/account/
# # http://localhost:8081/auth/realms/boxit/login-actions/authenticate?execution=ccb6603c-4536-4744-95ef-aa2ab2bc7a32&client_id=account&tab_id=pNMgGgYjRNA
keycloak:
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-keycloak:0.0.1
image: gitea.tail276105.ts.net/remsly/keycloak:4
networks:
boxitNet:
ipv4_address: 10.7.0.20
@@ -57,7 +57,7 @@ services:
- postgres
apache-oidc:
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-apache-oidc:0.0.3
image: gitea.tail276105.ts.net/remsly/apacheoidc:3
restart: always
networks:
boxitNet:
@@ -83,7 +83,7 @@ services:
- keycloak
elasticsearch:
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-elasticsearch:0.0.7
image: gitea.tail276105.ts.net/remsly/elasticsearch:3
restart: always
volumes:
- elasticsearch:/usr/share/elasticsearch/data
+30
View File
@@ -0,0 +1,30 @@
# Template for deploy/prod/.env
# Copy to .env and fill in the values: cp .env-template .env
# The .env file itself is git-ignored (it holds secrets).
# --- Database / Keycloak passwords ---
REMSLY_PSQL_PASSWORD=
REMSLY_PSQL_ADMIN_PASSWORD=
KC_PSQL_PASSWORD=
KC_ADMIN_PASSWORD=
# --- Tailscale ---
# Reusable, tag:server pre-authorized auth key from the Tailscale admin console.
TS_AUTHKEY=
TS_DNS_NAME=
# --- Apache OIDC (mod_auth_openidc) ---
OIDC_CRYPTO_PASSPHRASE=
OIDC_CLIENT_SECRET=
# --- Elasticsearch ---
ELASTIC_PASSWORD=
# --- Backend ---
# Keycloak client id (resource) the bearer-only backend expects.
KEYCLOAK_RESOURCE=
# --- Gitea registry (used by deploy.sh to download config.tar.gz) ---
# User + access token with package read scope for gitea.tail276105.ts.net.
GITEA_USER=
GITEA_TOKEN=
+1
View File
@@ -0,0 +1 @@
A0
@@ -0,0 +1,44 @@
# Production config for the backend, mounted into the container and activated via
# SPRING_CONFIG_ADDITIONAL_LOCATION (see docker-compose.yml). Secrets are injected
# from the environment (.env) through ${...} placeholders, so this file is safe to commit.
spring.profiles.active=PROD
server.port=5050
# Postgres runs as service "postgres" on remslyNet; db/user "remsly" (see POSTGRES_MULTIPLE_DATABASES).
spring.datasource.url=jdbc:postgresql://postgres:5432/remsly
spring.datasource.username=remsly
spring.datasource.password=${REMSLY_PSQL_PASSWORD}
# Elasticsearch runs as service "elasticsearch" on remslyNet, plain HTTP on 9200.
boxit.elasticsearch.host=elasticsearch
boxit.elasticsearch.port=9200
boxit.elasticsearch.user=elastic
boxit.elasticsearch.pass=${ELASTIC_PASSWORD}
boxit.nominatim.contact-email=remsly@boxi.ch
spring.jackson.date-format="yyyy-MM-dd HH:mm:ss"
management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
logging.level.root=INFO
keycloak.realm=Remsly
keycloak.bearer-only=true
keycloak.auth-server-url=https://remsly-id.tail276105.ts.net
keycloak.resource=${KEYCLOAK_RESOURCE}
# Bearer-only JWT validation via Spring Security OAuth2 resource server.
# issuer-uri : compared against the token's "iss" claim (the public https hostname, set by
# KC_HOSTNAME) - a pure string check, no network call.
# jwk-set-uri : where the signing keys are fetched. The https tailnet URL is NOT reachable from
# this container (tailscale serve binds only the tailnet IP), so pull the JWKS
# directly over the bridge on plain http :8080. Keycloak serves the same keys there
# regardless of the hostname in the request.
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://remsly-id.tail276105.ts.net/realms/Remsly
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://remsly-id.tail276105.ts.net:8080/realms/Remsly/protocol/openid-connect/certs
-1
View File
@@ -1 +0,0 @@
*.pem
@@ -1 +0,0 @@
{"creation_dt": "2023-03-24T14:43:02Z", "creation_host": "20e2283fb076"}
@@ -1 +0,0 @@
{"n": "u6an4MsyALnC0Ik2b8tcVpikTwzWgtASLGEUg7wEs7_piMKI7wNwroRbCesHujhJu2Ha_hDMrJmCEvT0T2fO4JzoAt1bLE3zWM4sXnWEpTXCrWxayjXbGTblrJcTIQPb7cbiTvyoAQvcThApZcvQEBkKZrlOp3aB62thM2zSI6XFVcbmF3CRCVJdIYN2K3ioQjkkxiAFwQ0EUNWtHzaDvjwFBvwAD2iDDqZh0mHYWWnhwV0okuJ3FBX8fVUr2oskq5cHhc8t8r8l1Iob9lEcgwkRJE93hQ1CMxYT4RX_rbJtyIFbixVkxM99x16nlyJnAWlPzEZrTz_fdcdFeQHPrw", "e": "AQAB", "d": "F7GUUvL1LlhGO_SHVhhUYW7GmGCjmULSeJ4NbBDKezPJNgZyKdPCX5bAtADkKeQ_kxRpBVlzrnt3jBM34erWmU_Wj74JbA-voU_vFEI8bGvcV-9ti0i_kHB5EZDFzo5hL6XEH3sRb4hi2cx6narda5rt0EinoMd64hHd5RNheTSWsVcSbBwzgDqKNB-OTr2vGzK3O3ZmY-I6IE5nVTf1tLkkI3GlPwKOSix1nDFtwHvKV0QTazsBop9MkMDwrBNVYJB_0G6AEtC8RXn9z4FIBZc9BAk4G8bygATwixu3WfUuM5LI3qavwILmQVFzmBfojHHWYf-eQBXf-kwR4KeX2Q", "p": "7MgYLilx05H3tvpy0M5ty2R6i3cE9vzl-eiNPM_rBF-eu-cAjkCtLViKg6VTdr0I8qV4B-NCshPpnF50cuFupF6jTgWZJeAfiOoEWQC6mqNxngX4N98mxDm7lf_3QqRsdhh2cS_5IOJ4cpaJ5WlX89WZvpfAKbCISQ_nejSypQM", "q": "yuG2pUTwPi4QazqX6w3oMAb9C-R6kDoYMNhRm2qCkSMQ9jJlaW80sIypYNzeD2JbF6pAjhF2mN2ehorMwVs97flNrSjGTdMts63JMcI6-w5MOWHmI938knxrBn5ku88QsJpjMdY4f25Oynb5LG3YSiNqQVztc_aC3qElrJ4CvOU", "dp": "PbwBye9WVSImotdzngtkRvZgmQIkEj1nvJusT2GqVc6UdUtKMsycGkmOe7pcI4Yv0gvgWrYfCoz0iKd71wP9s-9Uxp_hCSYBMtJI5eexv2XD1r6Zs_-DfP_Mb9R35DE4ciX4o5QtcSx62O108JJhsxpK5o1sNbJoJhx2br9UoH8", "dq": "TBt6UetXmGvdWuxjGxlfdczZVoHCfKJeesxbaMr-6gVWkpT7OB8wi95ol96jKvBBrdH_X4wrilLw6hXdk9dvlvtD6WZ3ZbCH33SdGh5707xcWdZg6CmZWzZzS79UeKPH5gg0l_h9v0gZJJiXBtS6I62_aX-70Bm0pGGdaXDpJAU", "qi": "L4HAI6WvpBjiwvGGH9IAoxsxqlnCton8uUUf2IQwpZMKe-d9LXPsOdWsyaZneCnc2C_AQsgLkpZIWd47koybwch4vTkbmLrNwtaaazVk53Axdg3ksFT-uWad4YTiqCH-hIt_2xXxvensMqHwRvIk44MHk0kq485nPfGx4E9zvDE", "kty": "RSA"}
@@ -1 +0,0 @@
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/94925864"}
@@ -1 +0,0 @@
{"creation_dt": "2023-03-24T14:44:14Z", "creation_host": "ed2dd2431723"}
@@ -1 +0,0 @@
{"n": "5JoKajoOVE7y5Bcchd4KPlVarooN-XLHtEbFRy0zrFG4mAY_Qf-H1eZkmHvPQwv-tDEPFRlabrxXqC3uWoy9uOmqXea7rbnOqQHYRSAhIiW8I0JbuzGHga-ZwujShQDj85eimjBXRKhjD6r3zRElchTWXyeT0_dRrOZ099zAE0qNCnVRxel6IXdFEKz9YrBvIqvEzJC2DBWwF0W6ZBlpiM8RXq1guBU22k0XkoDHE8d_LL6AMrhtXSCPwiL9YOxiyvSTgLHciy4c-WP1-mypy65oK5YLCiVagX4kM8qbxW_0nmFtdYCdhhEBrHktxofHUaps4cPGK_XDkbz5mti9vQ", "e": "AQAB", "d": "D53KSllu8yl1hElj5G03q-xuxm-d5sWFiPYwsd0Po5ztjE0Y8yw-nkkf5B7JGN5v5uVXD2zs9ZWeM1n3522n5ELqJYIGniaMTJVYrdg1Cz0irhHxbEdE54IUcfEvuHBIIjLgo8HbpFAQfFiElxitwy5TOqrceWVyyenwYaJN6MNU6h5_TReay7Smye1NQYnv3E63jDX3Od_rQyrLBV0UE3Y7BOPLGmqHTeeEaDo8zut0HgXIS0ecRrfTt8RkDpv_Xc9AAVzZaPXLNo4Kxo7YWCpoi32oLwUhsBELS6G3i2rQxPTveMnkeU0-1qo6hfoGZFMKpAivl4QX86_D_41tzQ", "p": "_dmRjJn2dVbPNOgvXeencRykjCHk5aRDYDCJ6BKYEsXpOdOZCGH2HlX-43Loh1w5qHcCXlSCX5T1GsOv-EAye2T9Eg-xfpkPHUJ9JlKRk1Oiy6FjFfBr_ObUzK_rC-WyHzcIaU50hyzwoKDhICRdbQvfJnOp02tE1Hlxm15qO28", "q": "5om5xnQAv6RRzkCdsO4Sk2yFXwoTvpL8KS9LG1RqcPoAvy1ey5oEPJLIUyFUmf8-hr5RW2OBgEwGCR3HQgaXXiyxeCSdoM0roEJaEcerk2i8JW_w_S52evvigJb4mOajzjY--WsSa12LpJTTKvSlf-9eWNaNeb7vv9GT1atWs5M", "dp": "lL_gFTbG_909kDAaSXmMWbDCD0dxPMuzm3L17KKwsBd-4h7HBu14bjBQKvHTpjX5p2kNXz9AIkhEFEnlHKBptHrp_KJrjNadz7R9Z25s1Zrlw1Mvn9SBQvH0cnkHT7JqXPY_nI2XpPpFiqqbIN2OTW7udTbDphque58qXjPGJks", "dq": "hP4o1Mpw8B1sL-7oyXvd7XwqSJR4pXUH0P5-doqxSPGAYslaqVv1W7hVpldRBfHQcUzzhCQYPhodVT7_hyBkTCi-TI_a-zvBEheU_DLS0RwXhgVY4cXfBb2vgf0V9qb_EmQK2johixyRBWXxmBNAMli1dcx4GybCCkdt4FKSls8", "qi": "Y_CRKqA-X8fnYAU9WIBVB2mH08m5y_3UhcmzJpcDAeaPygrh4hItYKdBSk9R9IBRiME84nIDW2Xor9c_GPpXcEItsHAHYHiF12hZtZHcEXyp9fFsJzprTWhXr1JRohlsPzCK6pzcB1Hk2ZwUvc0G4wkcaeOfo9LTVQj5eWRlnWQ", "kty": "RSA"}
@@ -1 +0,0 @@
{"body": {}, "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/1025448747"}
-14
View File
@@ -1,14 +0,0 @@
This directory contains your keys and certificates.
`[cert name]/privkey.pem` : the private key for your certificate.
`[cert name]/fullchain.pem`: the certificate file used in most server software.
`[cert name]/chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
`[cert name]/cert.pem` : will break many server configurations, and should not be used
without reading further documentation (see link below).
WARNING: DO NOT MOVE OR RENAME THESE FILES!
Certbot expects these files to remain in this location in order
to function properly!
We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
@@ -1,14 +0,0 @@
This directory contains your keys and certificates.
`privkey.pem` : the private key for your certificate.
`fullchain.pem`: the certificate file used in most server software.
`chain.pem` : used for OCSP stapling in Nginx >=1.3.7.
`cert.pem` : will break many server configurations, and should not be used
without reading further documentation (see link below).
WARNING: DO NOT MOVE OR RENAME THESE FILES!
Certbot expects these files to remain in this location in order
to function properly!
We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.
@@ -1,13 +0,0 @@
# renew_before_expiry = 30 days
version = 0.40.1
archive_dir = /etc/letsencrypt/archive/app.boxi.ch
cert = /etc/letsencrypt/live/app.boxi.ch/cert.pem
privkey = /etc/letsencrypt/live/app.boxi.ch/privkey.pem
chain = /etc/letsencrypt/live/app.boxi.ch/chain.pem
fullchain = /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
# Options used in the renewal process
[renewalparams]
account = ea5cf7614a600d23b821c0af24b17a1d
authenticator = standalone
server = https://acme-v02.api.letsencrypt.org/directory
@@ -1,132 +0,0 @@
<VirtualHost *:80>
DocumentRoot "/var/www/app"
ServerName app.boxi.ch
# ServerAlias www.${server_name}
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
LogLevel debug
<Directory />
AllowOverride none
Options -Indexes -Includes
Require all granted
<LimitExcept GET POST HEAD>
deny from all
</LimitExcept>
</Directory>
<Directory "/var/www/app">
AllowOverride None
Options -Indexes -Includes
Require all granted
</Directory>
<Location /status >
SetHandler server-status
</Location>
<Location / >
Redirect / https://app.boxi.ch/
</Location>
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
DocumentRoot "/var/www/app"
ServerName app.boxi.ch
#ServerAlias www.${server_name}
LogLevel info
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self' ; \
script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
style-src 'self' use.fontawesome.com 'unsafe-inline'; \
font-src 'self' data: use.fontawesome.com; \
base-uri 'self'; \
form-action 'self'; \
frame-ancestors 'none'; \
img-src 'self' data:; \
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
Header always append X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy "same-origin"
# this feature is not yet used and must be extended if needed, currently only a sample config
Header always set Feature-Policy "payment 'none';"
<Directory />
AllowOverride none
Options -Indexes -Includes
Require all granted
<LimitExcept GET POST HEAD>
deny from all
</LimitExcept>
</Directory>
<Directory "/var/www/app">
AllowOverride None
Options -Indexes -Includes
Require all granted
</Directory>
<Location /status >
SetHandler server-status
</Location>
RequestHeader set X-Forwarded-User expr=%{REMOTE_USER}
RequestHeader set Authorization "Bearer %{oidc_access_token}e"
Header set User-Details "%{REMOTE_USER}e %{oidc_claim_email}e"
OIDCProviderMetadataURL ${KC_ADDR}/realms/${KC_REALM}/.well-known/openid-configuration
OIDCClientID ${OIDC_CLIENT_ID}
OIDCClientSecret ${OIDC_CLIENT_SECRET}
OIDCRefreshAccessTokenBeforeExpiry 10
OIDCRedirectURI ${OIDC_REDIRECT_URI}
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
OIDCInfoHook ${OIDC_INFO_HOOK}
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fapp.boxi.ch
OIDCRemoteUserClaim preferred_username
OIDCStateMaxNumberOfCookies 3 true
<Location /status >
SetHandler server-status
</Location>
<Location />
AuthType openid-connect
Require valid-user
</Location>
# If the browser is allowed to cache index.html, it starts loading assets/scripts/images before apache had a chance
# to check whether or not the user is even authenticated, leading to many failed requests.
# Disabling cache for index.html forces the browser to fetch it first, and therefore ensures that the full
# authentication mechanic is being run.
# See also https://github.com/zmartzone/mod_auth_openidc/issues/113
<filesMatch "index.html">
FileETag None
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Mon, 21 Mar 1983 14:10:00 UTC"
</filesMatch>
#ProxyPass /ws ${FRONTEND_WS_URL}:${FRONTEND_PORT}/ws retry=0 timeout=86400
#ProxyPassReverse /ws ws://${FRONTEND_URL}:${FRONTEND_PORT}/ws
#ProxyPass /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/ retry=0
#ProxyPassReverse /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/
#ProxyPass / ${FRONTEND_URL}:${FRONTEND_PORT}/ retry=0
#ProxyPassReverse / ${FRONTEND_URL}:${FRONTEND_PORT}
SSLEngine On
# SSLUseStapling On
SSLProtocol -all +TLSv1.2
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
SSLHonorCipherOrder on
SSLCertificateFile /etc/letsencrypt/live/app.boxi.ch/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/app.boxi.ch/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
</VirtualHost>
</IfModule>
@@ -1,79 +0,0 @@
<VirtualHost *:80>
DocumentRoot "/var/www/id"
ServerName id.boxi.ch
# ServerAlias www.${server_name}
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
LogLevel info
<Directory />
AllowOverride none
Options -Indexes -Includes
Require all granted
<LimitExcept GET POST HEAD>
deny from all
</LimitExcept>
</Directory>
<Directory "/var/www/id">
AllowOverride None
Options -Indexes -Includes
Require all granted
</Directory>
<Location /status >
SetHandler server-status
</Location>
<Location / >
Redirect / https://id.boxi.ch/
</Location>
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
DocumentRoot "/var/www/id"
ServerName id.boxi.ch
#ServerAlias www.${server_name}
LogLevel info
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self' ; \
style-src 'self' 'unsafe-inline'; \
script-src 'self' 'unsafe-inline'; \
img-src 'self'; \
connect-src 'self'; \
form-action 'self'; \
frame-src http:; \
frame-ancestors 'self'; \
object-src 'none'; \
upgrade-insecure-requests;"
#ProxyRequests Off
#ProxyPreserveHost On
#ProxyAddHeaders On
#SSLProxyEngine On
#SSLProxyVerify none
#SSLProxyCheckPeerName off
#SSLProxyCheckPeerCN off
#SSLProxyCheckPeerName off
#SSLProxyCheckPeerExpire off
#RequestHeader set X-Forwarded-Proto "https"
#RequestHeader set X-Forwarded-Port "443"
#RequestHeader set X-Forwarded-Host "id.boxi.ch"
#ProxyPass / http://keycloak:8080/ retry=0
#ProxyPassReverse / http://keycloak:8080
<Location / >
Redirect / https://id.boxi.ch:8443/
</Location>
SSLEngine On
SSLProtocol -all +TLSv1.2
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
SSLHonorCipherOrder on
SSLCertificateFile /etc/letsencrypt/live/app.boxi.ch/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/app.boxi.ch/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/app.boxi.ch/fullchain.pem
</VirtualHost>
</IfModule>
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boxi the APP</title>
<style>
body {
margin: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
align-items: center;
}
</style>
</head>
<body>
<h1>This is the webserver for app.boxi.ch</h1>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boxi the ID</title>
<style>
body {
margin: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
align-items: center;
}
</style>
</head>
<body>
<h1>This is the webserver for id.boxi.ch</h1>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
#
# Boxit/Remsly prod deployment helper.
#
# One script, two modes:
# * Bootstrap (`deploy <version>`): downloads config-<version>.tar.gz from the
# Gitea generic package registry into ./config, extracts it to a staging dir
# and re-executes the deploy.sh *from the tarball* in install mode. The install
# logic therefore always comes from the deployed version (self-updating), which
# is what lets a release carry its own migration/special steps.
# * Install (`__install`, internal): syncs the staged config into the deploy
# directory (never touching .env or ./config) and runs docker compose.
#
# The deploy directory is the folder this script lives in; it also holds .env
# (secrets, host-only) and docker-compose.yml. Ship this script once to the host,
# afterwards every deploy refreshes it from the tarball.
#
# Usage:
# ./deploy.sh <version> deploy a specific version (e.g. 2026.07-A0-6)
# ./deploy.sh deploy <version> same, explicit
# ./deploy.sh list-remote [n] list the last n versions in the registry (default 10)
# ./deploy.sh list-local list downloaded config archives
# ./deploy.sh help
#
set -euo pipefail
# --- Constants ---------------------------------------------------------------
GITEA_HOST="gitea.tail276105.ts.net"
OWNER="remsly"
PKG="prod-config"
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${DEPLOY_DIR}/config"
INSTALLED_FILE="${DEPLOY_DIR}/.installed-version"
# --- Helpers -----------------------------------------------------------------
die() { echo "ERROR: $*" >&2; exit 1; }
# Read a single key from .env without sourcing it (values are unquoted KEY=value).
read_env() {
local key="$1" val
[ -f "${DEPLOY_DIR}/.env" ] || return 1
val="$(grep -E "^${key}=" "${DEPLOY_DIR}/.env" | head -n1 | cut -d= -f2-)" || return 1
# strip trailing CR (CRLF files) and surrounding quotes
val="${val%$'\r'}"; val="${val%\"}"; val="${val#\"}"; val="${val%\'}"; val="${val#\'}"
printf '%s' "$val"
}
# Basic-auth curl against Gitea, using GITEA_USER/GITEA_TOKEN from .env.
gitea_curl() {
local user token
user="$(read_env GITEA_USER || true)"
token="$(read_env GITEA_TOKEN || true)"
[ -n "$user" ] && [ -n "$token" ] || die "GITEA_USER/GITEA_TOKEN missing in ${DEPLOY_DIR}/.env"
curl -fsSL --user "${user}:${token}" "$@"
}
# Resolve the docker compose command (v2 plugin preferred, v1 fallback).
compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
die "neither 'docker compose' nor 'docker-compose' found"
fi
}
tarball_url() { printf 'https://%s/api/packages/%s/generic/%s/%s/config.tar.gz' "$GITEA_HOST" "$OWNER" "$PKG" "$1"; }
# --- Commands ----------------------------------------------------------------
cmd_list_remote() {
local n="${1:-10}"
echo "Last ${n} remote version(s) of ${PKG}:"
gitea_curl "https://${GITEA_HOST}/api/v1/packages/${OWNER}?type=generic&q=${PKG}" \
| grep -o '"version":"[^"]*"' | cut -d'"' -f4 \
| sort -Vr | head -n "$n" \
| sed 's/^/ /'
}
cmd_list_local() {
local current="" f v
[ -f "$INSTALLED_FILE" ] && current="$(cat "$INSTALLED_FILE")"
echo "Local config archives in ${CONFIG_DIR}:"
if ! ls "${CONFIG_DIR}"/config-*.tar.gz >/dev/null 2>&1; then
echo " (none)"
return 0
fi
for f in "${CONFIG_DIR}"/config-*.tar.gz; do
v="$(basename "$f")"; v="${v#config-}"; v="${v%.tar.gz}"
if [ "$v" = "$current" ]; then
echo " ${v} <- installed"
else
echo " ${v}"
fi
done | sort -Vr
}
cmd_deploy() {
local version="${1:-}"
[ -n "$version" ] || die "version required (e.g. ${0##*/} 2026.07-A0-6)"
mkdir -p "$CONFIG_DIR"
local archive="${CONFIG_DIR}/config-${version}.tar.gz"
if [ -f "$archive" ]; then
echo "Using cached ${archive}"
else
echo "Downloading ${version} ..."
gitea_curl -o "$archive" "$(tarball_url "$version")" \
|| { rm -f "$archive"; die "download of ${version} failed"; }
fi
local stage
stage="$(mktemp -d)"
tar -xzf "$archive" -C "$stage"
[ -f "${stage}/deploy.sh" ] || die "tarball has no deploy.sh"
# Hand off to the deploy.sh shipped in this version (self-updating install logic).
# The running host copy is not overwritten because we exec the staged copy.
exec bash "${stage}/deploy.sh" __install "$version" "$DEPLOY_DIR" "$stage"
}
cmd_install() {
local version="$1" deploydir="$2" stage="$3"
echo "Installing ${version} into ${deploydir} ..."
# Sync staged config into the deploy dir; never clobber secrets or downloads.
if command -v rsync >/dev/null 2>&1; then
rsync -a --exclude='.env' --exclude='config/' "${stage}/" "${deploydir}/"
else
# cp fallback: copy everything except .env / config (tarball has neither, but be safe)
( cd "$stage" && find . -mindepth 1 -maxdepth 1 \
! -name '.env' ! -name 'config' \
-exec cp -a {} "${deploydir}/" \; )
fi
( cd "$deploydir" && compose pull && compose up -d )
# Optional per-version migration/special-task hook shipped inside the tarball.
if [ -f "${deploydir}/post-deploy.sh" ]; then
echo "Running post-deploy.sh hook ..."
bash "${deploydir}/post-deploy.sh" "$version"
fi
printf '%s\n' "$version" > "${deploydir}/.installed-version"
rm -rf "$stage"
echo "Deployed ${version}."
}
usage() {
local me="${0##*/}"
cat <<EOF
${me} - Boxit/Remsly prod deployment helper
Usage:
${me} <version> deploy a specific version (e.g. 2026.07-A0-6)
${me} deploy <version> same, explicit
${me} list-remote [n] list the last n versions in the registry (default 10)
${me} list-local list downloaded config archives under ./config
${me} help
Requires GITEA_USER/GITEA_TOKEN in ./.env for registry access.
EOF
}
# --- Dispatch ----------------------------------------------------------------
main() {
local cmd="${1:-help}"
case "$cmd" in
list-remote) shift; cmd_list_remote "$@" ;;
list-local) cmd_list_local ;;
deploy) shift; cmd_deploy "$@" ;;
__install) shift; cmd_install "$@" ;;
help|-h|--help) usage ;;
*) cmd_deploy "$cmd" ;; # bare version shorthand
esac
}
main "$@"
+122 -64
View File
@@ -1,98 +1,156 @@
version: '3'
volumes:
postgres:
driver: local
elasticsearch:
driver: local
tailscale-id:
driver: local
tailscale-apache:
driver: local
networks:
remslyNet:
name: remslyNet
external: false
services:
postgres:
image: postgres:14-alpine
restart: always
networks:
boxitNet:
ipv4_address: 10.5.1.1
- remslyNet
environment:
POSTGRES_USER: 'boxitadmin'
POSTGRES_PASSWORD: 'kQsQKCQoa.yf.XiGCh.3'
POSTGRES_USER: 'admin'
POSTGRES_PASSWORD: ${REMSLY_PSQL_ADMIN_PASSWORD}
PGDATA: '/var/lib/postgresql/data/pgdata'
POSTGRES_MULTIPLE_DATABASES: 'boxit:q_VpgQaeXcjns9PqPmGj:boxit,keycloak:roraJK_ds3KDiHkMRLqf:keycloak'
POSTGRES_MULTIPLE_DATABASES: 'remsly:${REMSLY_PSQL_PASSWORD}:remsly,keycloak:${KC_PSQL_PASSWORD}:keycloak'
volumes:
- ./data/postgres:/var/lib/postgresql/data/pgdata
- ./config/postgres/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh
- postgres:/var/lib/postgresql/data/pgdata
- ./postgres/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh
keycloak:
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-keycloak:0.0.1
networks:
boxitNet:
ipv4_address: 10.5.0.20
image: gitea.tail276105.ts.net/remsly/keycloak:4
restart: always
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: 'admin'
KC_BOOTSTRAP_ADMIN_PASSWORD: 'dzVTry4QhwKBnmM8X2PWZRZA'
KC_HTTPS_CERTIFICATE_FILE: '/opt/keycloak/certs/fullchain.pem'
KC_HTTPS_CERTIFICATE_KEY_FILE: '/opt/keycloak/certs/privkey.pem'
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
KC_HTTP_ENABLED: 'true'
KC_DB: postgres
KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak'
KC_DB_USERNAME: 'keycloak'
KC_DB_PASSWORD: 'roraJK_ds3KDiHkMRLqf'
KC_HOSTNAME: 'id.boxi.ch'
# KC_HOSTNAME_STRICT: 'false'
# KC_PROXY_HEADERS: xforwarded
# KC_HTTP_ENABLED: 'true'
# KC_PROXY: edge
KC_DB_PASSWORD: ${KC_PSQL_PASSWORD}
# TLS is terminated by the tailscale-id sidecar; Keycloak runs plain HTTP (8080) behind it
KC_HOSTNAME: 'https://remsly-id.tail276105.ts.net'
KC_HOSTNAME_URL: 'https://remsly-id.tail276105.ts.net'
KC_HOSTNAME_ADMIN_URL: 'https://remsly-id.tail276105.ts.net'
# Serve backchannel endpoints (token/jwks/userinfo) under the requesting host so
# in-cluster clients can reach Keycloak over the bridge on :8080, while frontend
# URLs (issuer, authorization endpoint, logout) stay on the public https hostname.
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: 'true'
command:
- start
- '--optimized'
# - '--hostname-debug=true' # https://id.boxi.ch:8443/realms/master/hostname-debug
# - '--hostname-debug=true' #https://remsly-id.tail276105.ts.net/realms/master/hostname-debug
volumes:
- ./config/keycloak/theme:/opt/keycloak/themes/boxit
- ./config/keycloak/theme.v2:/opt/keycloak/themes/boxit.v2
- ./certbot/etc/live/app.boxi.ch/fullchain.pem:/opt/keycloak/certs/fullchain.pem:ro
- ./certbot/etc/live/app.boxi.ch/privkey.pem:/opt/keycloak/certs/privkey.pem:ro
ports:
- '8443:8443'
- ./keycloak/theme:/opt/keycloak/themes/boxit
- ./keycloak/theme.v2:/opt/keycloak/themes/boxit.v2
network_mode: service:tailscale-id
depends_on:
- postgres
- tailscale-id
apache-oidc:
image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-frontend:2023.1.36
# <<VERSION>> is replaced with the build version by the Package-Prod-Config CI job.
image: gitea.tail276105.ts.net/remsly/remsly-frontend:<<VERSION>>
restart: always
networks:
boxitNet:
ipv4_address: 10.5.0.30
environment:
OIDC_CRYPTO_PASSPHRASE: '39938b0a-1bd2-4352-937d-3cf1e2023727'
KC_ADDR: 'https://id.boxi.ch:8443'
KC_REALM: 'boxit'
OIDC_CLIENT_ID: 'apacheOIDC'
OIDC_CLIENT_SECRET: 'Ymye3T55SnH1YfssvZZi6tRIIZ0PJTsK'
OIDC_REDIRECT_URI: 'https://app.boxi.ch/openidc'
OIDC_CRYPTO_PASSPHRASE: ${OIDC_CRYPTO_PASSPHRASE}
# Browser-facing base URL (issuer, login/logout redirects) - reachable by the browser.
KC_ADDR: 'https://remsly-id.tail276105.ts.net'
# Server-side base URL for the OIDC metadata + backchannel calls (token exchange, jwks).
# tailscale serve only listens on the tailnet IP, so the https:443 URL is unreachable
# from this container; reach Keycloak directly over the bridge on plain http :8080.
KC_INTERNAL_ADDR: 'http://remsly-id.tail276105.ts.net:8080'
KC_REALM: 'Remsly'
OIDC_CLIENT_ID: 'ApacheOIDC'
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
OIDC_REDIRECT_URI: 'https://remsly.tail276105.ts.net/openidc'
APACHE_DEBUG_LEVEL: 'warn'
BACKEND_URL: 'backend'
BACKEND_URL: 'http://backend'
BACKEND_PORT: '5050'
volumes:
- ./config/frontend/apache2.conf:/etc/apache2/apache2.conf
- ./certbot/etc:/etc/letsencrypt
- ./data/www:/var/www
- ./config/frontend/app.boxi.ch.conf:/etc/apache2/sites-enabled/app.boxi.ch.conf
- ./config/frontend/id.boxi.ch.conf:/etc/apache2/sites-enabled/id.boxi.ch.conf
ports:
- '80:80'
- '443:443'
- ./frontend/apache2.conf:/etc/apache2/apache2.conf
- ./frontend/app.boxi.ch.conf:/etc/apache2/sites-enabled/app.boxi.ch.conf
network_mode: service:tailscale-apache
depends_on:
- keycloak
- backend
- tailscale-apache
#backend:
#image: boxi.registry.jetbrains.space/p/boxit/containers/boxit-backend:2023.1.36
# restart: always
# networks:
# boxitNet:
# ipv4_address: 10.5.0.40
# depends_on:
# - postgres
# - keycloak
backend:
# <<VERSION>> is replaced with the build version by the Package-Prod-Config CI job.
image: gitea.tail276105.ts.net/remsly/remsly-backend:<<VERSION>>
restart: always
environment:
# The CI-built image ships no application.properties, so mount one and activate it.
SPRING_CONFIG_ADDITIONAL_LOCATION: 'file:/config/application.properties'
# Secrets referenced as ${...} placeholders inside the mounted properties file.
REMSLY_PSQL_PASSWORD: ${REMSLY_PSQL_PASSWORD}
ELASTIC_PASSWORD: ${ELASTIC_PASSWORD}
KEYCLOAK_RESOURCE: ${KEYCLOAK_RESOURCE}
volumes:
- ./backend/application.properties:/config/application.properties:ro
networks:
- remslyNet
depends_on:
- postgres
- elasticsearch
networks:
boxitNet:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.5.0.0/16
elasticsearch:
image: gitea.tail276105.ts.net/remsly/elasticsearch:3
restart: always
volumes:
- elasticsearch:/usr/share/elasticsearch/data
networks:
- remslyNet
environment:
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- discovery.type=single-node
- xpack.license.self_generated.type=basic
tailscale-id:
image: tailscale/tailscale:latest
hostname: remsly-id
environment:
TS_AUTHKEY: ${TS_AUTHKEY}
TS_USERSPACE: "true"
TS_STATE_DIR: /var/lib/tailscale
TS_SERVE_CONFIG: /config/serve.json
TS_EXTRA_ARGS: --advertise-tags=tag:server
volumes:
- tailscale-id:/var/lib/tailscale
- ./tailscale/serve-id.json:/config/serve.json
networks:
remslyNet:
# Let the backend reach Keycloak via its external URL over the internal network:
# the FQDN resolves to this sidecar, hits tailscale serve on :443 (valid LE cert)
# and is proxied to Keycloak. Keeps the token issuer == auth-server-url.
aliases:
- remsly-id.tail276105.ts.net
restart: unless-stopped
tailscale-apache:
image: tailscale/tailscale:latest
hostname: remsly
environment:
TS_AUTHKEY: ${TS_AUTHKEY}
TS_USERSPACE: "true"
TS_STATE_DIR: /var/lib/tailscale
TS_SERVE_CONFIG: /config/serve.json
TS_EXTRA_ARGS: --advertise-tags=tag:server
volumes:
- tailscale-apache:/var/lib/tailscale
- ./tailscale/serve-apache.json:/config/serve.json
networks:
- remslyNet
restart: unless-stopped
@@ -1,7 +1,12 @@
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
# Include ports.conf
Listen 80
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
DefaultRuntimeDir ${APACHE_RUN_DIR}
PidFile ${APACHE_PID_FILE}
ServerName app.boxi.ch
ServerAdmin online@boxi.ch
+93
View File
@@ -0,0 +1,93 @@
<VirtualHost *:80>
DocumentRoot "/var/www/html/app"
ServerName remsly.tail276105.ts.net
# ServerAlias www.${server_name}
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
LogLevel info
# TLS is terminated by the tailscale sidecar; make mod_auth_openidc and the
# app aware that the external scheme is https.
RequestHeader set X-Forwarded-Proto "https"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set Content-Security-Policy "default-src 'self' ; \
script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
style-src 'self' use.fontawesome.com 'unsafe-inline'; \
font-src 'self' data: use.fontawesome.com; \
base-uri 'self'; \
form-action 'self'; \
frame-ancestors 'none'; \
img-src 'self' api.mapbox.com data:; \
upgrade-insecure-requests" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
Header always append X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy "same-origin"
# this feature is not yet used and must be extended if needed, currently only a sample config
Header always set Feature-Policy "payment 'none';"
<Directory />
AllowOverride none
Options -Indexes -Includes
Require all granted
<LimitExcept GET POST HEAD>
deny from all
</LimitExcept>
</Directory>
<Directory "/var/www/html/app">
AllowOverride None
Options -Indexes -Includes
Require all granted
# SPA deep-link support: serve index.html (HTTP 200) for any path that does not map to a
# real file, so the Angular router can resolve it client-side. Existing assets are still
# served directly; /api/ (proxied) and /openidc (mod_auth_openidc) are unaffected.
FallbackResource /index.html
</Directory>
RequestHeader set X-Forwarded-User expr=%{REMOTE_USER}
RequestHeader set Authorization "Bearer %{oidc_access_token}e"
Header set User-Details "%{REMOTE_USER}e %{oidc_claim_email}e"
# Fetch metadata + run the backchannel (token exchange, jwks) over the internal http:8080
# URL; the discovery doc still advertises the https issuer/authorization/logout endpoints
# (KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true on Keycloak keeps those frontend URLs public).
OIDCProviderMetadataURL ${KC_INTERNAL_ADDR}/realms/${KC_REALM}/.well-known/openid-configuration
OIDCClientID ${OIDC_CLIENT_ID}
OIDCClientSecret ${OIDC_CLIENT_SECRET}
OIDCRefreshAccessTokenBeforeExpiry 10
OIDCRedirectURI ${OIDC_REDIRECT_URI}
OIDCCryptoPassphrase ${OIDC_CRYPTO_PASSPHRASE}
# Where to land after a successful login when no original target URL is known (e.g. SSO entry
# via the redirect URI). Without this, mod_auth_openidc aborts with "Invalid Request".
OIDCDefaultURL https://remsly.tail276105.ts.net/
OIDCDefaultLoggedOutURL ${KC_ADDR}/realms/${KC_REALM}/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fremsly.tail276105.ts.net
OIDCRemoteUserClaim preferred_username
OIDCStateMaxNumberOfCookies 3 true
<Location /status >
SetHandler server-status
</Location>
<Location />
AuthType openid-connect
Require valid-user
</Location>
# If the browser is allowed to cache index.html, it starts loading assets/scripts/images before apache had a chance
# to check whether or not the user is even authenticated, leading to many failed requests.
# Disabling cache for index.html forces the browser to fetch it first, and therefore ensures that the full
# authentication mechanic is being run.
# See also https://github.com/zmartzone/mod_auth_openidc/issues/113
<filesMatch "index.html">
FileETag None
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Mon, 21 Mar 1983 14:10:00 UTC"
</filesMatch>
ProxyPass /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/ retry=0
ProxyPassReverse /api/ ${BACKEND_URL}:${BACKEND_PORT}/api/
</VirtualHost>
File renamed without changes.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.