Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 319 additions & 44 deletions .github/workflows/release-build.yml

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@ RUN go mod download && go mod verify
# Copy source code
COPY . .

# Build
# Build. FIPS=1 selects the arc-fips variant: same source/commit/version,
# compiled with the fips build tag against the CMVP-certified Go Cryptographic
# Module (GOFIPS140=v1.0.0) and baking in GODEBUG=fips140=only. The binary is
# always named "arc" inside the image; the FIPS-ness is conveyed by the image
# TAG (e.g. :<version>-fips), per "FIPS is a build variant, not a version".
ARG VERSION
RUN go build -v \
-tags=duckdb_arrow \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o arc ./cmd/arc
ARG FIPS=0
RUN if [ "$FIPS" = "1" ]; then \
echo "Building FIPS variant (GOFIPS140=v1.0.0, -tags=duckdb_arrow,fips)"; \
GOFIPS140=v1.0.0 CGO_ENABLED=1 go build -v \
-tags=duckdb_arrow,fips \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o arc ./cmd/arc; \
else \
CGO_ENABLED=1 go build -v \
-tags=duckdb_arrow \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o arc ./cmd/arc; \
fi

# Production stage
FROM debian:bookworm-slim
Expand Down
30 changes: 29 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
.PHONY: help build test run clean install deps fmt lint
.PHONY: help build build-fips test test-fips run clean install deps fmt lint fips-check

# Variables
BINARY_NAME=arc
GO=go
GOFLAGS=-v -tags=duckdb_arrow
MAIN_PATH=./cmd/arc

# FIPS build variant. Same source/commit/version as the standard build — only
# the build tag and the GOFIPS140 module selection differ. GOFIPS140=v1.0.0 is
# the CMVP-certified Go Cryptographic Module snapshot (see
# $(shell go env GOROOT)/lib/fips140/certified.txt). The fips tag enables
# fail-closed legacy-token verification and bakes in GODEBUG=fips140=only.
FIPS_BINARY_NAME=arc-fips
FIPS_GOFLAGS=-v -tags=duckdb_arrow,fips
GOFIPS140_VERSION=v1.0.0

help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
Expand All @@ -22,12 +31,30 @@ install: ## Install dependencies (alias for deps)
build: ## Build the binary
$(GO) build $(GOFLAGS) -o $(BINARY_NAME) $(MAIN_PATH)

build-fips: ## Build the FIPS 140-3 variant (arc-fips) against the certified Go module
GOFIPS140=$(GOFIPS140_VERSION) CGO_ENABLED=1 $(GO) build $(FIPS_GOFLAGS) -o $(FIPS_BINARY_NAME) $(MAIN_PATH)

fips-check: ## Verify the fips build links no non-FIPS crypto (x/crypto/bcrypt, x/crypto/hkdf)
@echo "Checking fips build import graph for non-approved crypto..."
@out=$$($(GO) list $(FIPS_GOFLAGS) -deps $(MAIN_PATH) 2>&1); \
if [ $$? -ne 0 ]; then \
echo "ERROR: go list failed (fips build does not compile?):"; echo "$$out"; exit 1; \
fi; \
if echo "$$out" | grep -E 'golang.org/x/crypto/(bcrypt|hkdf)'; then \
echo "ERROR: fips build pulls in non-FIPS crypto above"; exit 1; \
else \
echo "OK: no x/crypto/bcrypt or x/crypto/hkdf in the fips build"; \
fi
Comment thread
xe-nvdk marked this conversation as resolved.

run: ## Run Arc directly (without building)
$(GO) run $(GOFLAGS) $(MAIN_PATH)

test: ## Run all tests
$(GO) test $(GOFLAGS) -race -coverprofile=coverage.out ./...

test-fips: ## Run all tests with the fips build tag (exercises fail-closed paths)
$(GO) test $(FIPS_GOFLAGS) ./...

test-coverage: test ## Run tests with coverage report
$(GO) tool cover -html=coverage.out

Expand All @@ -43,6 +70,7 @@ lint: ## Run linter (requires golangci-lint)

clean: ## Clean build artifacts
rm -f $(BINARY_NAME)
rm -f $(FIPS_BINARY_NAME)
rm -f coverage.out
rm -rf ./data/arc/*

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,30 @@ go build -tags=duckdb_arrow ./cmd/arc
./arc
```

### FIPS 140-3 Build

For US defense/federal and other regulated environments, Arc ships an optional
**`arc-fips`** build: the same source at the same version, compiled against the
CMVP-certified Go Cryptographic Module and run in FIPS-only mode. Pick the
`-fips` artifact instead of the standard one.

```bash
# Binary — download arc-fips-linux-amd64 (or -arm64) from the release
# Container — same repos, -fips tag suffix:
docker run -d -p 8000:8000 -v arc-data:/app/data ghcr.io/basekick-labs/arc:VERSION-fips
# or basekicklabs/arc:VERSION-fips

# Build from source:
make build-fips # -> arc-fips (GOFIPS140=v1.0.0, -tags=duckdb_arrow,fips)
```

The FIPS build reports the same version as the standard build and logs
`"fips_mode":true` at startup. **Cutover note:** existing bcrypt-hashed API
tokens must be rotated when moving to the FIPS build (it stores new tokens with
PBKDF2 and fails bcrypt verification closed). The Go Cryptographic Module is
CMVP-certified; Arc itself is not a CMVP-listed module. See the
[FIPS 140-3 mode guide](https://docs.basekick.net/docs/configuration/fips).

---

## Ecosystem & Integrations
Expand Down Expand Up @@ -267,6 +291,8 @@ go build -tags=duckdb_arrow ./cmd/arc
- **Data Management**: GDPR-compliant delete operations
- **Observability**: Prometheus metrics, structured logging, graceful shutdown
- **Reliability**: Circuit breakers, retry with exponential backoff
- **Supply chain**: SBOM (SPDX + CycloneDX), Trivy scans, cosign-signed releases, SLSA L3 provenance
- **FIPS 140-3**: Optional `arc-fips` build against the CMVP-certified Go Cryptographic Module — see [Installation](#fips-140-3-build)
- **Edge Sync** (coming 26.09.1): Spoke-to-hub data transport for disconnected operations

---
Expand Down
13 changes: 13 additions & 0 deletions RELEASE_NOTES_2026.06.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ The deliverable is signed scan evidence attached to each release, not "zero find
- Container images on GHCR are signed by manifest digest. Verify with: `cosign verify ghcr.io/basekick-labs/arc:VERSION --certificate-identity-regexp "^https://github.com/Basekick-Labs/arc/" --certificate-oidc-issuer https://token.actions.githubusercontent.com`
- `arc-VERSION.intoto.jsonl` — [SLSA Level 3](https://slsa.dev/spec/v1.0/levels) provenance attestation for the release binaries, generated by the [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator). Proves who built the artifact, from what source commit, and with what build inputs. Verify with: `slsa-verifier verify-artifact arc-linux-amd64 --provenance-path arc-VERSION.intoto.jsonl --source-uri github.com/Basekick-Labs/arc`

**FIPS 140-3 build variant (`arc-fips`).** This release adds an optional FIPS build of Arc for US defense/aerospace and other regulated environments that require validated cryptography. The FIPS variant is the **same source at the same version** as the standard build — it is not a separate product or version line. It is compiled with a `fips` build tag against the **CMVP-certified Go Cryptographic Module v1.0.0** (`GOFIPS140=v1.0.0`) and runs the module in FIPS-only mode (`GODEBUG=fips140=only` is baked into the binary). It carries the same Arrow/DuckDB performance build (`duckdb_arrow`) as the standard build.

- **Artifacts:** `arc-fips-linux-amd64`, `arc-fips-linux-arm64` (each with a cosign `.bundle`); container images tagged `:VERSION-fips` on GHCR and Docker Hub; `arc-fips` `.deb` and `.rpm` packages (which `Conflicts`/`Provides`/`Replaces` the standard `arc` package — install one or the other; both ship `/usr/bin/arc` + `arc.service`); plus a FIPS container SBOM (`arc-VERSION-fips-sbom-container.spdx.json`) and FIPS Trivy report (`arc-VERSION-fips-trivy-report.json`). The standard `arc-linux-*` artifacts, `:VERSION` images, and `arc` packages are unchanged. SLSA provenance covers all four binaries (standard + FIPS). Both variants report the same version (e.g. `26.06.2`); the FIPS variant is identified by its artifact name and by `"fips_mode":true` in its startup log, not by a different version string.
- **What runs through the FIPS module:** TLS for the API, cluster, and MQTT paths is restricted to FIPS-approved cipher suites and curves; API-token hashing uses **PBKDF2-HMAC-SHA256** (replacing bcrypt, which is not FIPS-approved); cluster replication key derivation uses the standard-library `crypto/hkdf`. The FIPS binary links **no** non-approved crypto (verified in CI by an import-graph check) and **fails closed at startup** if it is not actually running in FIPS mode.
- **Token-hash verification is bounded against abuse:** the PBKDF2 verifier rejects an oversized or malformed hash with a cheap length check *before* decoding, caps the iteration count, and pins salt/key lengths — so a corrupted or hostile stored hash cannot drive a CPU/memory denial of service at auth time. Cluster token replication applies the same length caps on both the create and rotate paths, so a rogue node cannot persist an oversized hash into peers' state. These guards apply to **both** build variants.
- **Token rotation on cutover (required):** API tokens created by a non-FIPS build are stored as bcrypt hashes, which the FIPS build **refuses to verify** (it denies the request and logs the reason at debug level). When moving a deployment to the FIPS build, **rotate (recreate) existing API tokens** so they are re-stored as PBKDF2 hashes. New tokens are PBKDF2 automatically.
- **Crypto boundary:** all cryptography is provided by the Go Cryptographic Module. DuckDB and SQLite perform no cryptography (SQLite is used for token *storage*, not encryption) and are outside the module boundary.
- **CMVP status (read carefully):** Arc's FIPS build is compiled against the CMVP-**certified** Go Cryptographic Module v1.0.0. This means the *cryptographic module* is validated; Arc itself is **not** a CMVP-listed module, and this is **not** a claim that "Arc is FIPS 140-3 validated." Confirm the live certificate number on the [NIST CMVP list](https://csrc.nist.gov/projects/cryptographic-module-validation-program) before relying on it in a compliance submission.

Verify the FIPS binary: `cosign verify-blob arc-fips-linux-amd64 --bundle arc-fips-linux-amd64.bundle --certificate-identity-regexp "^https://github.com/Basekick-Labs/arc/" --certificate-oidc-issuer https://token.actions.githubusercontent.com`. Verify the FIPS image: `cosign verify ghcr.io/basekick-labs/arc:VERSION-fips --certificate-identity-regexp "^https://github.com/Basekick-Labs/arc/" --certificate-oidc-issuer https://token.actions.githubusercontent.com`.

Arc Enterprise customers get FIPS by running the `arc-fips` build with their license key — there is no separate enterprise FIPS binary.

## Bug fixes

**Pre-epoch (pre-1970) timestamps now partition into the correct hour (#312).** The ingest hour-bucketing used plain integer division (`time / microsecondsPerHour`), which truncates toward zero rather than flooring. A negative timestamp — a pre-1970 date, which Line Protocol and MessagePack both accept — was therefore filed one hour too late: a row at `1969-12-31 23:30` landed in the `1970/01/01/00/` partition instead of `1969/12/31/23/`, so a time-range query for the pre-1970 hour would miss it. Hour bucketing now floors toward negative infinity, so a timestamp always partitions into the hour that actually contains it; non-negative timestamps are unaffected (floor and truncation agree). The in-process CSV/Parquet import `partitions_created` count uses the same corrected bucketing. This fixes go-forward writes; any pre-1970 data already written by an affected build would need to be re-ingested or recompacted to move into the correct partition.
Expand Down
13 changes: 13 additions & 0 deletions cmd/arc/fips.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build fips

// This file is compiled only into the arc-fips build variant (-tags=fips).
//
// The //go:debug directive below bakes GODEBUG=fips140=only into the binary's
// default settings, so the arc-fips binary always runs the Go Cryptographic
// Module in FIPS-only mode — non-approved stdlib crypto calls fail closed, and
// an operator cannot silently downgrade the posture by omitting a GODEBUG env
// var. Pair this with building under GOFIPS140=v1.0.0 (the CMVP-certified
// module snapshot) so the compiled crypto IS the validated module source.
//
//go:debug fips140=only
package main
13 changes: 12 additions & 1 deletion cmd/arc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/basekick-labs/arc/internal/compaction"
"github.com/basekick-labs/arc/internal/config"
"github.com/basekick-labs/arc/internal/database"
"github.com/basekick-labs/arc/internal/fips"
"github.com/basekick-labs/arc/internal/governance"
"github.com/basekick-labs/arc/internal/ingest"
"github.com/basekick-labs/arc/internal/license"
Expand Down Expand Up @@ -92,7 +93,16 @@ func main() {

// Setup logger
logger.Setup(cfg.Log.Level, cfg.Log.Format)
log.Info().Str("version", Version).Bool("duckdb_arrow", database.ArrowEnabled).Msg("Starting Arc...")
log.Info().Str("version", Version).Bool("duckdb_arrow", database.ArrowEnabled).Bool("fips_mode", fips.Enabled()).Msg("Starting Arc...")

// Fail closed: the arc-fips build (fips.BuildTagged) MUST run with the Go
// Cryptographic Module actually in FIPS mode. The binary bakes in
// GODEBUG=fips140=only (see cmd/arc/fips.go), but an operator could still
// override it; refuse to start rather than silently run a "FIPS" binary
// outside FIPS mode, which would defeat the posture and mislead an auditor.
if fips.BuildTagged && !fips.Enabled() {
log.Fatal().Msg("FIPS build started without the Go Cryptographic Module in FIPS mode (GODEBUG=fips140 disabled); refusing to start. Remove any GODEBUG=fips140=off override.")
}

// Validate Enterprise License early (before component initialization)
// This allows us to apply core limits to DuckDB and ingestion workers
Expand Down Expand Up @@ -2660,6 +2670,7 @@ func main() {
Int("port", cfg.Server.Port).
Str("protocol", protocol).
Str("version", Version).
Bool("fips_mode", fips.Enabled()).
Msg("Arc is ready!")

// Wait for shutdown signal
Expand Down
47 changes: 46 additions & 1 deletion internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"crypto/tls"
"fmt"
"net"
"os"
Expand All @@ -13,6 +14,7 @@ import (
"time"

"github.com/basekick-labs/arc/internal/auth"
"github.com/basekick-labs/arc/internal/fips"
"github.com/basekick-labs/arc/internal/logger"
"github.com/basekick-labs/arc/internal/metrics"
"github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -498,7 +500,22 @@ func (s *Server) Start() error {
Str("cert_file", s.tlsCert).
Str("key_file", s.tlsKey).
Msg("TLS enabled - starting HTTPS server")
err = s.app.ListenTLS(addr, s.tlsCert, s.tlsKey)
if fips.BuildTagged {
// FIPS build only: build our own TLS listener instead of
// Fiber's app.ListenTLS (which constructs an opaque tls.Config
// we cannot influence). fips.HardenTLSConfig pins FIPS-approved
// versions/cipher-suites/curves — the only way to control the
// public API's negotiated suites for an auditor-defensible FIPS
// posture. The default build keeps Fiber's ListenTLS unchanged
// so standard-build TLS behavior is untouched.
var ln net.Listener
ln, err = s.hardenedTLSListener(addr)
if err == nil {
err = s.app.Listener(ln)
}
} else {
err = s.app.ListenTLS(addr, s.tlsCert, s.tlsKey)
}
} else {
err = s.app.Listen(addr)
}
Expand All @@ -511,6 +528,34 @@ func (s *Server) Start() error {
return nil
}

// hardenedTLSListener loads the server keypair and returns a TLS net.Listener
// whose tls.Config is restricted to FIPS-approved versions, cipher suites, and
// curves (see fips.HardenTLSConfig). Used instead of Fiber's app.ListenTLS so
// Arc — not Fiber — owns the public API's TLS policy.
//
// NextProtos advertises ONLY "http/1.1" over ALPN. Fiber's engine (fasthttp)
// is HTTP/1.1-only — it does not implement HTTP/2 server-side, and Fiber's own
// ListenTLS sets no h2 ALPN either — so advertising "h2" here would let a
// client negotiate a protocol the server cannot speak. Setting "http/1.1"
// explicitly matches what a default TLS server negotiates and avoids that
// mismatch. (The h2 references elsewhere in Arc are the outbound net/http
// clients for S3/cluster, not this server.)
func (s *Server) hardenedTLSListener(addr string) (net.Listener, error) {
cert, err := tls.LoadX509KeyPair(s.tlsCert, s.tlsKey)
if err != nil {
return nil, fmt.Errorf("load API TLS keypair: %w", err)
}
tlsCfg := fips.HardenTLSConfig(&tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{"http/1.1"},
})
Comment thread
xe-nvdk marked this conversation as resolved.
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("listen on %s: %w", addr, err)
}
return tls.NewListener(ln, tlsCfg), nil
}

// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(timeout time.Duration) error {
s.logger.Info().Msg("Shutting down server gracefully...")
Expand Down
Loading
Loading