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
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,77 @@ On the client we redirect git to the proxy:

As Git itself isn't aware of the snapshots, Git-specific code in the Cachew CLI can be used to reconstruct a repository.

## Authorization (OPA)

Cachew uses [Open Policy Agent](https://www.openpolicyagent.org/) (OPA) for request authorization. A default policy is
always active even without any configuration, allowing any request from 127.0.0.1 and `GET` and `HEAD` requests from
elsewhere.

To customise the policy, add an `opa` block to your configuration with either an inline policy or a path to a `.rego` file:

```hcl
# Inline policy
opa {
policy = <<EOF
package cachew.authz
default allow := false
allow if input.method == "GET"
allow if input.method == "HEAD"
allow if { input.method == "POST"; input.path[0] == "api" }
EOF
}

# Or reference an external file
opa {
policy-file = "./policy.rego"
}
```

Policies must be written under `package cachew.authz` and define a boolean `allow` rule. The input document available to policies contains:

| Field | Type | Description |
|---|---|---|
| `input.method` | string | HTTP method (GET, POST, etc.) |
| `input.path` | []string | URL path split by `/` (e.g. `["api", "v1", "object"]`) |
| `input.headers` | map[string]string | Request headers (lowercased keys) |
| `input.remote_addr` | string | Client address (ip:port) |

Since `remote_addr` includes the port, use `startswith` to match by IP:

```rego
allow if startswith(input.remote_addr, "127.0.0.1:")
```

Policies can reference external data that becomes available as `data.*` in Rego. Provide it inline via `data` or from a file via `data-file`:

```hcl
# Inline JSON data
opa {
policy-file = "./policy.rego"
data = <<EOF
{"allowed_cidrs": ["10.0.0.0/8"], "jwks": {"keys": [...]}}
EOF
}

# Or from a file
opa {
policy-file = "./policy.rego"
data-file = "./opa-data.json"
}
```

```json
{"allowed_cidrs": ["10.0.0.0/8"], "jwks": {"keys": [...]}}
```

```rego
package cachew.authz
default allow := false
allow if net.cidr_contains(data.allowed_cidrs[_], input.remote_addr)
```

If `data-file` is not set, `data.*` is empty but policies can still use `http.send` to fetch data at evaluation time.

## Docker

## Hermit
Expand Down
11 changes: 11 additions & 0 deletions cachew.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ log {
level = "debug"
}

opa {
policy = <<EOF
package cachew.authz
default allow := false
allow if input.method == "GET"
allow if input.method == "HEAD"
allow if startswith(input.remote_addr, "127.0.0.1:")

EOF
}

git-clone {}

# github-app {
Expand Down
17 changes: 12 additions & 5 deletions cmd/cachew/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import (
type CLI struct {
LoggingConfig logging.Config `embed:"" prefix:"log-"`

URL string `help:"Remote cache server URL." default:"http://127.0.0.1:8080"`
Platform bool `help:"Prefix keys with platform ($${os}-$${arch}-)."`
Daily bool `help:"Prefix keys with date ($${YYYY}-$${MM}-$${DD}-). Mutually exclusive with --hourly." xor:"timeprefix"`
Hourly bool `help:"Prefix keys with date and hour ($${YYYY}-$${MM}-$${DD}-$${HH}-). Mutually exclusive with --daily." xor:"timeprefix"`
URL string `help:"Remote cache server URL." default:"http://127.0.0.1:8080"`
Authorization string `help:"Authorization header value (e.g. 'Bearer <token>')."`
Platform bool `help:"Prefix keys with platform ($${os}-$${arch}-)."`
Daily bool `help:"Prefix keys with date ($${YYYY}-$${MM}-$${DD}-). Mutually exclusive with --hourly." xor:"timeprefix"`
Hourly bool `help:"Prefix keys with date and hour ($${YYYY}-$${MM}-$${DD}-$${HH}-). Mutually exclusive with --daily." xor:"timeprefix"`

Get GetCmd `cmd:"" help:"Download object from cache." group:"Operations:"`
Stat StatCmd `cmd:"" help:"Show metadata for cached object." group:"Operations:"`
Expand All @@ -42,7 +43,13 @@ func main() {
ctx := context.Background()
_, ctx = logging.Configure(ctx, cli.LoggingConfig)

remote := cache.NewRemote(cli.URL)
var headerFunc cache.HeaderFunc
if cli.Authorization != "" {
headerFunc = func() http.Header {
return http.Header{"Authorization": {cli.Authorization}}
}
}
remote := cache.NewRemote(cli.URL, headerFunc)
defer remote.Close()

kctx.BindTo(ctx, (*context.Context)(nil))
Expand Down
18 changes: 13 additions & 5 deletions cmd/cachewd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/block/cachew/internal/jobscheduler"
"github.com/block/cachew/internal/logging"
"github.com/block/cachew/internal/metrics"
"github.com/block/cachew/internal/opa"
"github.com/block/cachew/internal/reaper"
"github.com/block/cachew/internal/strategy"
"github.com/block/cachew/internal/strategy/git"
Expand All @@ -42,6 +43,7 @@ type GlobalConfig struct {
MetricsConfig metrics.Config `hcl:"metrics,block"`
GitCloneConfig gitclone.Config `hcl:"git-clone,block"`
GithubAppConfigs []githubapp.Config `hcl:"github-app,block,optional"`
OPAConfig opa.Config `hcl:"opa,block"`
}

type CLI struct {
Expand Down Expand Up @@ -102,7 +104,9 @@ func main() {

logger.InfoContext(ctx, "Starting cachewd", "bind", globalConfig.Bind)

server := newServer(ctx, mux, globalConfig.Bind, globalConfig.MetricsConfig)
server, err := newServer(ctx, mux, globalConfig.Bind, globalConfig.MetricsConfig, globalConfig.OPAConfig)
kctx.FatalIfErrorf(err)

err = server.ListenAndServe()
kctx.FatalIfErrorf(err)
}
Expand Down Expand Up @@ -191,15 +195,18 @@ func extractPathPrefix(path string) string {
return prefix
}

func newServer(ctx context.Context, muxHandler http.Handler, bind string, metricsConfig metrics.Config) *http.Server {
logger := logging.FromContext(ctx)

func newServer(ctx context.Context, muxHandler http.Handler, bind string, metricsConfig metrics.Config, opaConfig opa.Config) (*http.Server, error) {
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labeler, _ := otelhttp.LabelerFromContext(r.Context())
labeler.Add(attribute.String("cachew.http.path.prefix", extractPathPrefix(r.URL.Path)))
muxHandler.ServeHTTP(w, r)
})

handler, err := opa.Middleware(ctx, opaConfig, handler)
if err != nil {
return nil, errors.Errorf("initialise OPA middleware: %w", err)
}

// Add standard otelhttp middleware
handler = otelhttp.NewMiddleware(metricsConfig.ServiceName,
otelhttp.WithMeterProvider(otel.GetMeterProvider()),
Expand All @@ -208,6 +215,7 @@ func newServer(ctx context.Context, muxHandler http.Handler, bind string, metric

handler = httputil.LoggingMiddleware(handler)

logger := logging.FromContext(ctx)
return &http.Server{
Addr: bind,
Handler: handler,
Expand All @@ -220,7 +228,7 @@ func newServer(ctx context.Context, muxHandler http.Handler, bind string, metric
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
return logging.ContextWithLogger(ctx, logger.With("client", c.RemoteAddr().String()))
},
}
}, nil
}

// loadGlobalConfig unmarshals the global config from HCL, using a two-pass
Expand Down
25 changes: 24 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/goproxy/goproxy v0.25.0
github.com/lmittmann/tint v1.1.3
github.com/minio/minio-go/v7 v7.0.98
github.com/open-policy-agent/opa v1.14.1
github.com/prometheus/client_golang v1.23.2
go.etcd.io/bbolt v1.4.3
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0
Expand All @@ -22,22 +23,33 @@ require (
)

require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/aofei/backoff v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand All @@ -46,21 +58,32 @@ require (
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/vektah/gqlparser/v2 v2.5.32 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

require (
Expand Down
Loading