fix: move newsfeed-api off 8081 to 22672 (conflict-unlikely port)
All checks were successful
deploy / build-web (push) Successful in 1m34s
deploy / deploy-web (push) Successful in 4s
deploy / build-api (push) Successful in 6m17s
deploy / deploy-api (push) Successful in 10s

8081 is the alt-HTTP port — one of the crowded defaults every proxy/dev server
grabs — so it's collision-prone on a shared host. Move to 22672, a registered-
range port in the sparse 20000s band, derived deterministically from the
service name and recorded in architecture port-allocations.md.

Updated everywhere the number appears: api bind config + default, firewalld
service XML, nginx upstream, vite dev proxy, deploy workflow API_PORT,
infra-setup (SELinux semanage label), and docs.

Operator note: re-run script/infra-setup.sh on slartibartfast to relabel the
SELinux port (22672) and ship the updated firewalld XML before the next deploy
restarts the api on the new port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fKZzDpvjiJ9eYbPGgJvUP
This commit is contained in:
2026-07-08 12:25:26 +03:00
parent 19dfe793e0
commit 81fe5f7d4d
9 changed files with 21 additions and 20 deletions

View File

@@ -30,7 +30,7 @@ concurrency:
env: env:
API_HOST: slartibartfast.kosherinata.internal API_HOST: slartibartfast.kosherinata.internal
WEB_HOST: oolon.kosherinata.internal WEB_HOST: oolon.kosherinata.internal
API_PORT: "8081" API_PORT: "22672"
SSH_OPTS: -o StrictHostKeyChecking=accept-new SSH_OPTS: -o StrictHostKeyChecking=accept-new
jobs: jobs:

View File

@@ -57,7 +57,8 @@ Crypto/token logic lives in `core::auth`; the flow orchestration in `core::servi
api + worker → `slartibartfast.kosherinata.internal`; SPA → `oolon.kosherinata.internal` api + worker → `slartibartfast.kosherinata.internal`; SPA → `oolon.kosherinata.internal`
(nginx serves + proxies `/v1` to the API over the mesh; TLS terminates at oolon). API (nginx serves + proxies `/v1` to the API over the mesh; TLS terminates at oolon). API
port 8081, plain HTTP behind firewalld. See `.gitea/workflows/deploy.yml` (infra truth) port 22672 (registered in architecture `port-allocations.md`; deliberately not the crowded
`8081`), plain HTTP behind firewalld. See `.gitea/workflows/deploy.yml` (infra truth)
and `script/infra-setup.sh` (one-time host prep). `Type=exec`, not `notify`, because axum and `script/infra-setup.sh` (one-time host prep). `Type=exec`, not `notify`, because axum
doesn't `sd_notify`. doesn't `sd_notify`.

View File

@@ -7,7 +7,7 @@
# Bind the mesh-routable address so the oolon proxy can reach it. firewalld # Bind the mesh-routable address so the oolon proxy can reach it. firewalld
# (newsfeed-api service) bounds who may connect; TLS terminates at oolon. # (newsfeed-api service) bounds who may connect; TLS terminates at oolon.
bind = "0.0.0.0:8081" bind = "0.0.0.0:22672"
database_path = "/var/lib/newsfeed/newsfeed.db" database_path = "/var/lib/newsfeed/newsfeed.db"
session_ttl_days = 30 session_ttl_days = 30

View File

@@ -3,5 +3,5 @@
<short>newsfeed-api</short> <short>newsfeed-api</short>
<description>newsfeed REST/JSON API. Reached over the WireGuard mesh by the oolon edge <description>newsfeed REST/JSON API. Reached over the WireGuard mesh by the oolon edge
proxy, which terminates TLS and reverse-proxies to this port.</description> proxy, which terminates TLS and reverse-proxies to this port.</description>
<port protocol="tcp" port="8081"/> <port protocol="tcp" port="22672"/>
</service> </service>

View File

@@ -7,7 +7,7 @@
# ln -sf ../sites-available/newsfeed.internal.conf /etc/nginx/sites-enabled/ # ln -sf ../sites-available/newsfeed.internal.conf /etc/nginx/sites-enabled/
upstream newsfeed_api { upstream newsfeed_api {
server slartibartfast.kosherinata.internal:8081; server slartibartfast.kosherinata.internal:22672;
keepalive 16; keepalive 16;
} }

View File

@@ -30,7 +30,7 @@ pub struct Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
bind: "127.0.0.1:8081".parse().expect("valid default bind addr"), bind: "127.0.0.1:22672".parse().expect("valid default bind addr"),
database_path: PathBuf::from("/var/lib/newsfeed/newsfeed.db"), database_path: PathBuf::from("/var/lib/newsfeed/newsfeed.db"),
session_ttl_days: 30, session_ttl_days: 30,
max_db_connections: 5, max_db_connections: 5,
@@ -42,7 +42,7 @@ impl Default for Config {
impl Config { impl Config {
/// Load configuration, layering an optional TOML file and the environment over the /// Load configuration, layering an optional TOML file and the environment over the
/// defaults. Env vars are `NEWSFEED_` prefixed (e.g. `NEWSFEED_BIND=0.0.0.0:8081`). /// defaults. Env vars are `NEWSFEED_` prefixed (e.g. `NEWSFEED_BIND=0.0.0.0:22672`).
pub fn load(file: Option<&Path>) -> anyhow::Result<Self> { pub fn load(file: Option<&Path>) -> anyhow::Result<Self> {
let mut fig = Figment::from(Serialized::defaults(Config::default())); let mut fig = Figment::from(Serialized::defaults(Config::default()));
if let Some(path) = file { if let Some(path) = file {

View File

@@ -65,9 +65,9 @@ cd web && pnpm install && pnpm build
## Run locally ## Run locally
```sh ```sh
# 1. API (creates ./data/newsfeed.db, binds 127.0.0.1:8081) # 1. API (creates ./data/newsfeed.db, binds 127.0.0.1:22672)
NEWSFEED_DATABASE_PATH=./data/newsfeed.db \ NEWSFEED_DATABASE_PATH=./data/newsfeed.db \
NEWSFEED_BIND=127.0.0.1:8081 \ NEWSFEED_BIND=127.0.0.1:22672 \
NEWSFEED_COOKIE_SECURE=false \ NEWSFEED_COOKIE_SECURE=false \
cargo run -p newsfeed-api -- --config /nonexistent cargo run -p newsfeed-api -- --config /nonexistent
@@ -75,7 +75,7 @@ NEWSFEED_COOKIE_SECURE=false \
NEWSFEED_DATABASE_PATH=./data/newsfeed.db \ NEWSFEED_DATABASE_PATH=./data/newsfeed.db \
cargo run -p newsfeed-worker -- --config /nonexistent cargo run -p newsfeed-worker -- --config /nonexistent
# 3. Frontend dev server (proxies /v1 and /health to :8081) # 3. Frontend dev server (proxies /v1 and /health to :22672)
cd web && pnpm dev cd web && pnpm dev
``` ```
@@ -86,19 +86,19 @@ non-existent `--config` path just uses defaults + env, which is convenient for d
```sh ```sh
# register + login (cookie jar) # register + login (cookie jar)
curl -c cj -X POST localhost:8081/v1/auth/register -H content-type:application/json \ curl -c cj -X POST localhost:22672/v1/auth/register -H content-type:application/json \
-d '{"username":"me","email":"me@example.com","password":"hunter2hunter2"}' -d '{"username":"me","email":"me@example.com","password":"hunter2hunter2"}'
curl -c cj -X POST localhost:8081/v1/auth/login -H content-type:application/json \ curl -c cj -X POST localhost:22672/v1/auth/login -H content-type:application/json \
-d '{"identifier":"me","password":"hunter2hunter2"}' -d '{"identifier":"me","password":"hunter2hunter2"}'
# weight an interest, mint an ingest token # weight an interest, mint an ingest token
curl -b cj -X PUT localhost:8081/v1/interests -H content-type:application/json -d '{"label":"rust","weight":0.9}' curl -b cj -X PUT localhost:22672/v1/interests -H content-type:application/json -d '{"label":"rust","weight":0.9}'
TOKEN=$(curl -b cj -X POST localhost:8081/v1/tokens -H content-type:application/json -d '{"name":"agent"}' | jq -r .secret) TOKEN=$(curl -b cj -X POST localhost:22672/v1/tokens -H content-type:application/json -d '{"name":"agent"}' | jq -r .secret)
# an agent pushes a candidate; read the ranked feed # an agent pushes a candidate; read the ranked feed
curl -X POST localhost:8081/v1/ingest/candidates -H "authorization: Bearer $TOKEN" \ curl -X POST localhost:22672/v1/ingest/candidates -H "authorization: Bearer $TOKEN" \
-H content-type:application/json -d '{"external_id":"a1","title":"Rust 2.0 released"}' -H content-type:application/json -d '{"external_id":"a1","title":"Rust 2.0 released"}'
curl -b cj localhost:8081/v1/feed curl -b cj localhost:22672/v1/feed
``` ```
## API surface (`/v1`) ## API surface (`/v1`)

View File

@@ -9,7 +9,7 @@
# * a scoped /etc/sudoers.d/newsfeed_gitea_ci drop-in (visudo-verified) # * a scoped /etc/sudoers.d/newsfeed_gitea_ci drop-in (visudo-verified)
# and per role: # and per role:
# * API host (slartibartfast): the `newsfeed` service account, /etc/newsfeed + # * API host (slartibartfast): the `newsfeed` service account, /etc/newsfeed +
# /var/lib/newsfeed, SELinux port label for 8081, firewalld service # /var/lib/newsfeed, SELinux port label for 22672, firewalld service
# * WEB host (oolon): /var/www/newsfeed webroot, the internal `newsfeed.internal` TLS # * WEB host (oolon): /var/www/newsfeed webroot, the internal `newsfeed.internal` TLS
# cert (lair provisioner) + step@newsfeed renewal, and the nginx vhost # cert (lair provisioner) + step@newsfeed renewal, and the nginx vhost
# #
@@ -19,7 +19,7 @@ set -euo pipefail
# --- infra truth (edit here if the topology changes) ------------------------------- # --- infra truth (edit here if the topology changes) -------------------------------
API_HOST="slartibartfast.kosherinata.internal" # newsfeed-api + newsfeed-worker API_HOST="slartibartfast.kosherinata.internal" # newsfeed-api + newsfeed-worker
WEB_HOST="oolon.kosherinata.internal" # nginx: SPA + API reverse proxy WEB_HOST="oolon.kosherinata.internal" # nginx: SPA + API reverse proxy
API_PORT="8081" API_PORT="22672"
CERT_NAME="newsfeed" # dot-free; serves ${CERT_NAME}.internal CERT_NAME="newsfeed" # dot-free; serves ${CERT_NAME}.internal
PUBLIC_FQDN="rob.fyi" # WAN name (Let's Encrypt); "" to skip PUBLIC_FQDN="rob.fyi" # WAN name (Let's Encrypt); "" to skip
CERT_EMAIL="ops@rob.fyi" # certbot registration email CERT_EMAIL="ops@rob.fyi" # certbot registration email

View File

@@ -9,8 +9,8 @@ export default defineConfig({
server: { server: {
port: 5173, port: 5173,
proxy: { proxy: {
'/v1': 'http://127.0.0.1:8081', '/v1': 'http://127.0.0.1:22672',
'/health': 'http://127.0.0.1:8081', '/health': 'http://127.0.0.1:22672',
}, },
}, },
build: { build: {