Homelab Weekly: REST API with Bearer Tokens, Resource Limits, and PodSecurity Fixes

Kevn Lukritz | Aug 3, 2026 min read

DurpDeploy: Full REST API with Bearer Token Authentication

The biggest piece of work this week landed on Tuesday, July 29. Commit a7f6a339 introduced a complete JSON REST API surface for DurpDeploy, spanning roughly 104KB of diff across dozens of files. This is not a thin wrapper over the existing web handlers. It is a parallel API layer with its own authentication mechanism, its own middleware stack, and its own CLI subcommands for token lifecycle management.

Token Architecture

The authentication model uses bearer tokens with the prefix ddp_pat_ (DurpDeploy Personal Access Token). The token format is ddp_pat_ followed by 64 hex characters (32 bytes of cryptographic randomness from crypto/rand). The full token is 72 characters long.

The security design follows the GitHub model: the plaintext token is shown exactly once at creation time, and only a SHA-256 hash of the random portion is stored in the database. The MintApiToken function in internal/auth/apitoken.go returns four values: the full token string, a 12-character prefix (the ddp_pat_ prefix plus the first 4 hex characters, used for display and identification), the SHA-256 hash, and any error. The prefix is stored in the api_tokens table alongside the hash, so the admin UI can show “ddp_pat_ab12…” without ever storing or displaying the full token.

Token verification works by stripping the ddp_pat_ prefix, computing the SHA-256 hash of the remaining hex portion, and comparing against the stored hash. This means a database leak exposes only hashes, not usable tokens. The hash comparison uses a simple equality check rather than constant-time comparison, which is acceptable here because the token space is 256 bits and timing attacks on token lookup are not a realistic threat model for a self-hosted deployment tool.

Database Schema

The api_tokens table was added via migration and includes these columns: id (UUID), user_id (foreign key to users), name (human-readable label like “ci token”), token_prefix (12-char display prefix), token_hash (SHA-256 of the random portion), scope (reserved for future scope restrictions), last_used_at (updated on each authenticated request), expires_at (nullable, for future expiration support), created_at, and revoked_at (nullable, set when the token is revoked rather than deleted).

The sqlc-generated queries in internal/db/api_tokens.sql.go include CreateApiToken, GetApiTokenByHash, ListApiTokensByUser, RevokeApiToken, CountApiTokensByUser, and CountAllApiTokens. The ListApiTokensByUser query returns all tokens for a given user, which powers the /settings/tokens page where users can see their own tokens and revoke them.

API Middleware Stack

The API routes live under /api/v1/* and use a separate middleware chain from the web UI. The authentication middleware extracts the Authorization: Bearer *** header, strips the prefix, hashes the token, looks it up in the database, and injects the authenticated user into the request context. If the token is invalid, expired, or revoked, the middleware returns a 401 JSON response with {“error”: “invalid or expired token”}`.

A WriteBlockMiddleware function in internal/auth/apitoken.go provides the API equivalent of the CSRF middleware’s viewer block. Viewer-role users can perform GET requests but receive a 403 JSON response for any POST, PUT, or DELETE operation. This mirrors the web UI behavior where viewers can browse but cannot make changes.

The audit middleware was extended with API-specific action mappings. Every API endpoint has a corresponding audit action: POST /api/v1/tokens maps to create_api_token, DELETE /api/v1/tokens/{id} maps to revoke_api_token, POST /api/v1/projects/{id}/deploy maps to create_deployment, and so on. This ensures that API usage appears in the audit log alongside web UI actions, giving admins a unified view of all state-changing operations regardless of how they were triggered.

API Endpoints

The API exposes the core DurpDeploy operations as JSON endpoints. The project-scoped endpoints include creating releases, refreshing releases from git, triggering deployments, canceling deployments, approving deployments (for production environments that require approval), redeploying, and managing schedules. There are also global endpoints for listing deployments and managing API tokens.

All API responses use standard HTTP status codes and JSON bodies. Error responses follow the format {"error": "message"}. Success responses return the created or updated resource as JSON. The API does not use the templ-generated HTML views at all; it is a pure JSON API.

Swagger Documentation

The API includes auto-generated Swagger/OpenAPI documentation served at /api/swagger/. The implementation uses go-openapi/runtime to serve the Swagger UI assets, which are copied from node_modules/swagger-ui-dist into static/swagger-ui/ by the make swagger-ui-copy target. The swagger spec itself is generated by swagger generate spec from annotations in the handler code and written to internal/swagger/spec.json.

The swagger UI is embedded into the Go binary using //go:embed swagger-ui, which means the API documentation is always available on a running server without any external dependencies. The embed directive requires the files to exist at build time, which caused CI failures until the swagger-ui materialization step was added to the pipeline (more on that below).

CLI Token Management

The durpdeploy tokens subcommand provides CLI access to token lifecycle operations. The three subcommands are:

  • durpdeploy tokens create --user <email> --name <label>: Creates a new token for the specified user and prints the full plaintext token to stdout. This is the only time the full token is visible.
  • durpdeploy tokens list [--user <email>]: Lists all tokens (or tokens for a specific user), showing the prefix, name, creation date, last used date, and revocation status.
  • durpdeploy tokens revoke <prefix>: Revokes a token by its 12-character prefix. The token is soft-deleted by setting revoked_at rather than removed from the database, preserving the audit trail.

The CLI implementation in cmd/server/main.go opens a database connection, creates a repository, and delegates to the same query layer used by the web handlers. This ensures consistency between the CLI, web UI, and API.

Admin Navigation Update

Commit 5dba5d6e on July 30 added an “API tokens” link to the admin dropdown menu in the base template. The link points to /admin/tokens, which is the global token management page where admins can see and revoke tokens for any user. This is distinct from the per-user token management page at /settings/tokens where regular users manage their own tokens.

The test TestTokens_AdminDropdownLinks validates that the admin dropdown contains the link to /admin/tokens, ensuring the navigation remains intact across template changes.

CI Pipeline: Swagger UI Materialization

Commit 8ba0132b on July 29 fixed a CI failure caused by the swagger UI embed directive. The static/swagger-ui/ directory is gitignored because it contains build artifacts copied from node_modules/swagger-ui-dist. The //go:embed swagger-ui directive in static/static.go requires the files to exist at build time, but on a fresh CI checkout the directory is empty, causing go vet, go test, and go build to fail with “pattern swagger-ui: no matching files found”.

The fix adds make swagger-ui-copy to the before_script of the lint, test, and build stages in .gitlab-ci.yml. This installs Node.js and npm (via apk add), then runs the Makefile target that copies the swagger UI assets from node_modules into static/swagger-ui/. The same fix was applied to the GitHub Actions workflow in .github/workflows/release.yml, adding a “Materialize static/swagger-ui/” step before go vet and go test.

The Dockerfile was also updated to include swagger-ui-copy in the build step, ensuring the Docker image includes the swagger UI assets. The comment in the Dockerfile explicitly notes that swagger-ui-copy is the only step that materializes the directory; tailwind-build and js-build do not produce it.

The AGENTS.md documentation was updated to reflect the new requirement, noting that developers need to run make swagger-ui-copy locally before any go vet, go test, or go build on a fresh clone.

GitHub Actions: Buildx Cache Separation

Commit 501f0292 on July 29 fixed a GitHub Actions issue where the Docker buildx cache was polluting the main image package in GHCR. The previous configuration stored the cache in the same GHCR package as the container image, using tags like cache-<sha>. The problem is that buildx cache manifests have the media type application/vnd.buildkit.cacheconfig.v0, which is not an OCI image. When tools like docker run or podman run tried to pull the cache tag, they failed with “unsupported image-specific operation on artifact with type application/vnd.buildkit.cacheconfig.v0”.

The fix introduces a separate CACHE_IMAGE_NAME environment variable set to durpdeploy-cache, so the cache lives in its own GHCR package (ghcr.io/<owner>/durpdeploy-cache) rather than alongside the image. The cache references now use ghcr.io/${{ env.REGISTRY_OWNER }}/${{ env.CACHE_IMAGE_NAME }}:cache-${{ github.sha }} instead of the image package. The comment in the workflow explains that the first run after this change will not have a cache to import, but subsequent runs will reuse it.

The cache configuration uses mode=max for both cache-from and cache-to, which exports all intermediate layers. Per-SHA cache tags allow concurrent branches to run without racing on the same tag.

Go Toolchain and Dependency Updates

Commit 0ec31b4f on July 30 bumped the Go Docker image from golang:1.25-alpine to golang:1.26-alpine in both .gitlab-ci.yml and the Dockerfile. This is a straightforward toolchain update to pick up the latest Go release.

Commit 717c37de on July 29 added a .github/dependabot.yml configuration file for Dependabot version updates. The file specifies a weekly schedule for the root directory, though the package-ecosystem field is left empty with a comment pointing to the documentation. This appears to be a skeleton configuration that needs the ecosystem filled in (likely “gomod” for Go modules and “npm” for Node.js dependencies).

The go.mod file was updated with new dependencies for the Swagger/OpenAPI integration: github.com/go-openapi/runtime, github.com/google/uuid (moved from indirect to direct), and the full go-openapi dependency tree including analysis, errors, jsonpointer, jsonreference, loads, spec, strfmt, swag utilities, and validate. The golang.org/x/crypto dependency was bumped from v0.53.0 to v0.54.0, and github.com/go-logr/logr from v1.4.3 to v1.4.4.

Infra-Talos: Resource Limits and PodSecurity Fixes

The infra-talos cluster received three commits this week addressing resource management and PodSecurity compliance.

ArgoCD Controller Resource Limits

Commit 4ea53a6a on August 1 added resource requests and limits to the ArgoCD controller in infra-talos/argocd/values.yaml. The controller now requests 100m CPU and 256Mi memory, with a memory limit of 512Mi. This prevents the controller from consuming unbounded memory, which was causing OOM kills on the cluster nodes.

The ArgoCD controller is responsible for reconciling Application resources, which means it watches all Applications, pulls manifests from git repositories, and applies them to target clusters. This is a memory-intensive operation, especially when managing dozens of Applications across multiple clusters. The 512Mi limit should be sufficient for the current workload, but may need to be increased as the number of managed Applications grows.

Vault Server Resource Limits

Commit e1b49bac on July 30 added explicit resource requests and limits to the Vault server in infra-talos/vault/values.yaml. The previous configuration had only statefulset-level resources (256Mi memory, 250m CPU requests) but no server-level resources. The new configuration sets server requests to 1Gi memory and 500m CPU, with limits of 2Gi memory and 1000m CPU.

The old values.yaml had commented-out resource blocks referencing the Vault Reference Architecture for a Small Cluster (8Gi memory, 2000m CPU requests; 16Gi memory, 4000m CPU limits). Those values are appropriate for a production Vault cluster but are overkill for a homelab deployment. The chosen values (1Gi/2Gi) strike a balance between providing enough memory for Vault’s in-memory credential cache and avoiding resource waste on a cluster with limited capacity.

The statefulset-level resources were also adjusted downward from 256Mi/250m to 128Mi/100m requests, reflecting the fact that the statefulset wrapper (the Kubernetes pod management layer) does not need significant resources; the actual Vault process runs in the server container and is now constrained by the server-level resources.

OpenClarity HostPath Removal

Commit d78e999f on August 1 removed hostPath volume mounts from the OpenClarity container runtime discovery server in infra-talos/openclarity/values.yaml. The previous configuration mounted eight host paths for container runtime sockets and state directories: /var/run/containerd, /run/k3s/containerd, /var/lib/docker, /var/run/crio, /var/lib/containers, /var/run/containers, and /etc/containers.

The problem is that Talos Linux does not expose these paths on the host. Talos runs containerd in a restricted, immutable filesystem, and the traditional runtime paths do not exist. More critically, hostPath volumes violate the baseline PodSecurity policy enforced on the infra-talos cluster. The baseline policy prohibits hostPath volumes because they bypass namespace isolation and can expose sensitive host filesystem content to pods.

The fix sets containerRuntimePaths: [], effectively disabling the container runtime discovery feature. This means OpenClarity will not be able to discover and scan running containers on the node, but it can still scan container images pulled from registries. For a homelab environment where most workloads are deployed from known-good images, this is an acceptable tradeoff. If node-level container scanning is needed in the future, the proper solution is to configure OpenClarity to use the Kubernetes API for container discovery rather than host filesystem access.

The comment in the values.yaml explicitly notes that Talos has no traditional runtime paths and that hostPath volumes violate baseline PodSecurity, providing context for future maintainers who might wonder why the discovery feature is disabled.

Looking Ahead

The REST API is functional but lacks two features that would make it production-ready: token expiration and scope restrictions. The expires_at and scope columns exist in the database schema but are not enforced by the authentication middleware. Adding expiration support is straightforward: check expires_at during token verification and reject tokens past their expiration date. Scope restrictions are more complex and require defining a scope model (read-only, deploy-only, admin) and enforcing it in each handler.

The Swagger UI is embedded and served, but the spec generation step (swagger generate spec) is not integrated into the CI pipeline. The spec is generated locally and committed, which means it can drift from the actual API. Adding a CI check that regenerates the spec and fails if there are uncommitted changes would catch drift early.

On the infra-talos side, the ArgoCD controller memory limit of 512Mi should be monitored. If the controller starts hitting the limit and getting OOM-killed, the limit needs to be increased. The Vault server resources (1Gi/2Gi) are conservative and should be sufficient, but monitoring memory usage over the next week will confirm whether the chosen values are appropriate.

The OpenClarity hostPath removal disables node-level container scanning. If that capability is needed, the next step is to investigate whether OpenClarity supports Kubernetes API-based container discovery, or whether a different scanning approach (such as scanning images in the registry before they are pulled) would better fit the Talos security model.

The GitHub Actions cache separation should be verified by checking the GHCR packages after the next workflow run. The durpdeploy-cache package should appear with cache manifests, and the main durpdeploy package should contain only OCI image manifests.

The dependabot configuration needs the package-ecosystem field filled in. Based on the project’s dependencies, it should include “gomod” for Go modules and “npm” for Node.js dependencies. Once configured, dependabot will automatically create PRs for dependency updates, reducing the manual effort of tracking upstream releases.