Skip to content

Commit f1ac673

Browse files
committed
Harden API before 0.8.0 release
1 parent 3141d0e commit f1ac673

9 files changed

Lines changed: 460 additions & 81 deletions

File tree

.github/workflows/images.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Container Images
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
platforms:
7+
description: OCI platforms to build.
8+
required: true
9+
default: linux/amd64
10+
push:
11+
tags:
12+
- "v*"
13+
14+
permissions:
15+
contents: read
16+
packages: write
17+
18+
env:
19+
CARGO_TERM_COLOR: always
20+
21+
jobs:
22+
wolfi:
23+
name: Build and publish Wolfi image
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 60
26+
env:
27+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
28+
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@v6
32+
33+
- name: Set up QEMU
34+
uses: docker/setup-qemu-action@v4
35+
36+
- name: Set up Buildx
37+
uses: docker/setup-buildx-action@v4
38+
39+
- name: Log in to GitHub Container Registry
40+
uses: docker/login-action@v4
41+
with:
42+
registry: ghcr.io
43+
username: ${{ github.actor }}
44+
password: ${{ secrets.GITHUB_TOKEN }}
45+
46+
- name: Prepare tags
47+
id: tags
48+
shell: bash
49+
run: |
50+
set -euo pipefail
51+
short_sha="${GITHUB_SHA::12}"
52+
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
53+
version="${GITHUB_REF_NAME}"
54+
else
55+
version="dev"
56+
fi
57+
58+
tags=(
59+
"ghcr.io/${GITHUB_REPOSITORY}:${version}"
60+
"ghcr.io/${GITHUB_REPOSITORY}:${version}-wolfi"
61+
"ghcr.io/${GITHUB_REPOSITORY}:sha-${short_sha}-wolfi"
62+
)
63+
64+
if [[ "${GITHUB_REF_NAME}" == "${DEFAULT_BRANCH}" ]]; then
65+
tags+=(
66+
"ghcr.io/${GITHUB_REPOSITORY}:latest"
67+
"ghcr.io/${GITHUB_REPOSITORY}:latest-wolfi"
68+
)
69+
fi
70+
71+
{
72+
echo "tags<<EOF"
73+
printf '%s\n' "${tags[@]}"
74+
echo "EOF"
75+
} >> "${GITHUB_OUTPUT}"
76+
77+
- name: Build and publish Wolfi image
78+
uses: docker/build-push-action@v7
79+
with:
80+
context: .
81+
file: Dockerfile
82+
platforms: ${{ inputs.platforms || 'linux/amd64' }}
83+
push: true
84+
cache-from: type=gha,scope=wolfi
85+
cache-to: type=gha,mode=max,scope=wolfi
86+
labels: |
87+
org.opencontainers.image.title=hashavatar-api
88+
org.opencontainers.image.description=Public avatar API and demo website for hashavatar
89+
org.opencontainers.image.licenses=EUPL-1.2
90+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
91+
org.opencontainers.image.revision=${{ github.sha }}
92+
org.opencontainers.image.version=${{ github.ref_name }}
93+
tags: ${{ steps.tags.outputs.tags }}

Cargo.lock

Lines changed: 5 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hashavatar-api"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
edition = "2024"
55
rust-version = "1.95"
66
description = "Public avatar API and landing page for hashavatar"
@@ -12,11 +12,11 @@ documentation = "https://github.com/valkyoth/hashavatar-api"
1212
[dependencies]
1313
aws-config = { version = "1.8.16", default-features = false, features = ["rt-tokio"] }
1414
aws-sdk-s3 = { version = "1.131.0", default-features = false, features = ["rt-tokio", "sigv4a"] }
15-
hashavatar = { version = "0.7.0", features = ["blake3", "xxh3"] }
15+
hashavatar = { version = "0.8.0", features = ["blake3", "xxh3"] }
1616
image = { version = "0.25.10", default-features = false, features = ["png"] }
1717
axum = { version = "0.8.9", default-features = false, features = ["http1", "tokio", "query", "json"] }
18+
getrandom = { version = "0.4.2", default-features = false }
1819
ipnet = { version = "2.12.0", default-features = false }
19-
lru = { version = "0.18.0", default-features = false }
2020
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
2121
serde_json = { version = "1.0.149", default-features = false, features = ["std"] }
2222
sha2 = { version = "0.11.0", default-features = false, features = ["alloc"] }

Cargo.toml.split-template

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hashavatar-api"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
edition = "2024"
55
rust-version = "1.95"
66
description = "Public avatar API and landing page for hashavatar"
@@ -10,10 +10,10 @@ homepage = "https://github.com/valkyoth/hashavatar-api"
1010
documentation = "https://github.com/valkyoth/hashavatar-api"
1111

1212
[dependencies]
13-
hashavatar = { version = "0.7.0", features = ["blake3", "xxh3"] }
13+
hashavatar = { version = "0.8.0", features = ["blake3", "xxh3"] }
1414
axum = { version = "0.8.9", default-features = false, features = ["http1", "tokio", "query", "json"] }
15+
getrandom = { version = "0.4.2", default-features = false }
1516
ipnet = { version = "2.12.0", default-features = false }
16-
lru = { version = "0.18.0", default-features = false }
1717
serde = { version = "1.0.228", default-features = false, features = ["derive"] }
1818
serde_json = { version = "1.0.149", default-features = false, features = ["std"] }
1919
sha2 = { version = "0.11.0", default-features = false, features = ["alloc"] }

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ audit, SBOM, reproducibility, smoke, and GitHub CodeQL default setup checks.
1111

1212
## Current Status
1313

14-
The current service version is `0.7.0`.
14+
The current service version is `0.8.0`.
1515

1616
Implemented now:
1717

@@ -25,7 +25,7 @@ Implemented now:
2525
- Namespace-aware tenant and style-version parameters.
2626
- Selectable identity hash algorithms: `SHA-512`, `BLAKE3`, and `XXH3`.
2727
- `WebP`, `PNG`, `JPEG`, `GIF`, and `SVG` responses.
28-
- Avatar families from `hashavatar 0.7.0`: `cat`, `dog`, `robot`, `fox`,
28+
- Avatar families from `hashavatar 0.8.0`: `cat`, `dog`, `robot`, `fox`,
2929
`alien`, `monster`, `ghost`, `slime`, `bird`, `wizard`, `skull`, `paws`,
3030
`planet`, `rocket`, `mushroom`, `cactus`, `frog`, `panda`, `cupcake`,
3131
`pizza`, `icecream`, `octopus`, and `knight`.
@@ -55,7 +55,7 @@ Intentionally external:
5555
| Area | Status |
5656
| --- | --- |
5757
| Service license | `EUPL-1.2` |
58-
| Renderer crate | `hashavatar 0.7.0` |
58+
| Renderer crate | `hashavatar 0.8.0` |
5959
| MSRV | Rust `1.95.0` |
6060
| Runtime container | Wolfi |
6161
| HTTP framework | `axum` |
@@ -234,6 +234,8 @@ The repository includes:
234234
and internal error disclosure behavior
235235
- local HTTP smoke tests for health, SVG/PNG rendering, security headers, and
236236
invalid namespace rejection
237+
- local Podman smoke tests for the Wolfi image when
238+
`HASHAVATAR_API_GATE_PODMAN=1` is set
237239
- `cargo deny` dependency and license policy
238240
- RustSec advisory scanning
239241
- SPDX and CycloneDX SBOM generation
@@ -252,6 +254,16 @@ For self-hosting with Podman and Fluxheim, see:
252254
The compose example builds this service with the Wolfi runtime image and places
253255
Fluxheim in front of it as the public TLS reverse proxy.
254256

257+
Release tags publish a Wolfi runtime image to GitHub Container Registry:
258+
259+
```text
260+
ghcr.io/valkyoth/hashavatar-api:<version>
261+
ghcr.io/valkyoth/hashavatar-api:<version>-wolfi
262+
```
263+
264+
Manual workflow runs from the default branch also publish `dev`, `dev-wolfi`,
265+
`latest`, and `latest-wolfi` tags.
266+
255267
## Related Projects
256268

257269
- [`hashavatar`](https://crates.io/crates/hashavatar)

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Supported Versions
44

5-
Security fixes are expected for the latest published `0.7.x` release.
5+
Security fixes are expected for the latest published `0.8.x` release.
66

77
## Reporting a Vulnerability
88

scripts/podman_smoke.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5+
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hashavatar-api-podman.XXXXXX")
6+
IMAGE="${HASHAVATAR_API_IMAGE:-hashavatar-api:local-wolfi}"
7+
CONTAINER_NAME="hashavatar-api-smoke-$$"
8+
EXPECTED_UID="${HASHAVATAR_API_EXPECTED_UID:-10001}"
9+
KEEP_LOGS="${HASHAVATAR_API_PODMAN_KEEP_LOGS:-0}"
10+
11+
cleanup() {
12+
status=$?
13+
podman rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
14+
if [ "$KEEP_LOGS" = "1" ] || [ "$status" -ne 0 ]; then
15+
echo "podman smoke artifacts kept in $TMP_DIR" >&2
16+
else
17+
rm -rf "$TMP_DIR"
18+
fi
19+
}
20+
trap cleanup EXIT INT TERM
21+
22+
if [ -z "${CONTAINER_HOST:-}" ] && [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -S "$XDG_RUNTIME_DIR/podman/podman.sock" ]; then
23+
CONTAINER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
24+
export CONTAINER_HOST
25+
fi
26+
27+
port=$(python3 - <<'PY'
28+
import socket
29+
30+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
31+
try:
32+
sock.bind(("127.0.0.1", 0))
33+
print(sock.getsockname()[1])
34+
finally:
35+
sock.close()
36+
PY
37+
)
38+
39+
echo "podman smoke: image=$IMAGE port=$port"
40+
if [ -n "${CONTAINER_HOST:-}" ]; then
41+
echo "podman smoke: CONTAINER_HOST=$CONTAINER_HOST"
42+
fi
43+
44+
podman build -t "$IMAGE" -f "$ROOT_DIR/Dockerfile" "$ROOT_DIR"
45+
46+
podman run -d \
47+
--name "$CONTAINER_NAME" \
48+
-e PORT=8080 \
49+
-p "127.0.0.1:$port:8080" \
50+
"$IMAGE" >/dev/null
51+
52+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
53+
status="$(curl -sS -o "$TMP_DIR/health.json" -w '%{http_code}' "http://127.0.0.1:$port/healthz" 2>/dev/null || true)"
54+
if [ "$status" = "200" ]; then
55+
break
56+
fi
57+
sleep 0.3
58+
done
59+
60+
if [ "${status:-}" != "200" ]; then
61+
echo "podman smoke failed: healthz returned ${status:-no response}" >&2
62+
podman logs "$CONTAINER_NAME" >&2 || true
63+
exit 1
64+
fi
65+
66+
grep -q '"status":"ok"' "$TMP_DIR/health.json"
67+
grep -q '"service":"hashavatar-api"' "$TMP_DIR/health.json"
68+
69+
curl -sSf -D "$TMP_DIR/svg.headers" \
70+
"http://127.0.0.1:$port/v1/avatar?id=cat@hashavatar.app&algorithm=sha512&kind=cat&background=themed&format=svg&size=256" \
71+
-o "$TMP_DIR/avatar.svg"
72+
grep -q '^<svg ' "$TMP_DIR/avatar.svg"
73+
grep -qi '^content-type: image/svg+xml' "$TMP_DIR/svg.headers"
74+
grep -qi '^x-content-type-options: nosniff' "$TMP_DIR/svg.headers"
75+
76+
curl -sSf -D "$TMP_DIR/png.headers" \
77+
"http://127.0.0.1:$port/v1/avatar?id=robot@hashavatar.app&algorithm=blake3&kind=robot&background=white&format=png&size=128" \
78+
-o "$TMP_DIR/avatar.png"
79+
grep -qi '^content-type: image/png' "$TMP_DIR/png.headers"
80+
test -s "$TMP_DIR/avatar.png"
81+
82+
USER_LINE="$(podman run --rm --entrypoint /bin/sh "$IMAGE" -c id)"
83+
case "$USER_LINE" in
84+
*"uid=$EXPECTED_UID"* )
85+
;;
86+
* )
87+
echo "podman smoke failed: expected runtime uid=$EXPECTED_UID, got: $USER_LINE" >&2
88+
exit 1
89+
;;
90+
esac
91+
92+
echo "podman smoke: ok"

scripts/validate-security-invariants.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ fail() {
88

99
grep -q 'MAX_RATE_LIMIT_BUCKETS' src/main.rs \
1010
|| fail "rate limiter capacity constant is missing"
11-
grep -q 'LruCache' src/main.rs \
12-
|| fail "rate limiter must use bounded LRU storage"
11+
grep -q 'struct RateLimiterState' src/main.rs \
12+
|| fail "rate limiter bounded state is missing"
13+
grep -q 'evict_oldest_if_full' src/main.rs \
14+
|| fail "rate limiter must evict when the bucket bound is reached"
15+
grep -q 'VecDeque<String>' src/main.rs \
16+
|| fail "rate limiter must track oldest buckets for eviction"
1317
grep -q 'into_make_service_with_connect_info::<SocketAddr>' src/main.rs \
1418
|| fail "server must expose peer socket addresses to handlers"
1519
grep -q 'TRUSTED_PROXIES_ENV' src/main.rs \
@@ -53,6 +57,14 @@ grep -q 'build_avatar_asset_rejects_oversized_namespace' src/main.rs \
5357
|| fail "hashavatar namespace validation regression test is missing"
5458
grep -q 'object_key_uses_full_sha256_digest' src/main.rs \
5559
|| fail "full object-key digest regression test is missing"
60+
grep -q 'content_security_policy_uses_nonce_without_unsafe_inline' src/main.rs \
61+
|| fail "CSP nonce regression test is missing"
62+
grep -q 'render_json_ld_escapes_script_end_tags' src/main.rs \
63+
|| fail "JSON-LD script breakout regression test is missing"
64+
grep -q 'escape_html_attribute_handles_single_quotes' src/main.rs \
65+
|| fail "attribute escaping regression test is missing"
66+
grep -q 'etag_uses_full_sha256_digest' src/main.rs \
67+
|| fail "full ETag digest regression test is missing"
5668

5769
if [ -e .github/workflows/codeql.yml ] || [ -e .github/codeql/codeql-config.yml ]; then
5870
fail "CodeQL default setup is enabled in GitHub; remove repo-level advanced CodeQL configuration"

0 commit comments

Comments
 (0)