Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf216b61b | ||
|
|
9dbb3ffe07 |
@@ -26,11 +26,6 @@ make docker
|
|||||||
docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixa:latest
|
docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixa:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The example config allowlists no host (`allowlist_hosts`), so pixa serves an
|
|
||||||
image only for a signed or encrypted URL. Every image on an allowlisted host is
|
|
||||||
served to anyone without a signature, at any size and in any format, so
|
|
||||||
allowlist only hosts you control, where nobody else can put images.
|
|
||||||
|
|
||||||
A container takes its settings from environment variables (see Configuration
|
A container takes its settings from environment variables (see Configuration
|
||||||
below for the list). Only `PIXA_SIGNING_KEY` is required; if it is unset the
|
below for the list). Only `PIXA_SIGNING_KEY` is required; if it is unset the
|
||||||
container exits at startup naming the variable. Everything else has a built-in
|
container exits at startup naming the variable. Everything else has a built-in
|
||||||
@@ -120,9 +115,7 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for pixa needs:
|
|||||||
- `PIXA_SIGNING_KEY` (required): secret for signed and encrypted URLs and
|
- `PIXA_SIGNING_KEY` (required): secret for signed and encrypted URLs and
|
||||||
login, 32+ characters, for example from `openssl rand -base64 32`
|
login, 32+ characters, for example from `openssl rand -base64 32`
|
||||||
- `PIXA_ALLOWLIST_HOSTS`: upstream hosts served without a signature,
|
- `PIXA_ALLOWLIST_HOSTS`: upstream hosts served without a signature,
|
||||||
comma-separated; default none. Every image on a listed host is served to
|
comma-separated
|
||||||
anyone without a signature, so list only hosts you control, where nobody
|
|
||||||
else can put images.
|
|
||||||
- `PIXA_CACHE_MAX_BYTES`: disk cache limit in bytes; `0` disables it;
|
- `PIXA_CACHE_MAX_BYTES`: disk cache limit in bytes; `0` disables it;
|
||||||
default 75% of (free space + what the cache holds)
|
default 75% of (free space + what the cache holds)
|
||||||
- the rest are in the table under Configuration below
|
- the rest are in the table under Configuration below
|
||||||
|
|||||||
@@ -30,14 +30,6 @@ P2: security: per-origin rate limiting
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-15 the example config allowlists no host (closes #249): it allowlisted
|
|
||||||
`github.com` and `user-images.githubusercontent.com`, where anyone can put an
|
|
||||||
image, so a deployment set up as Getting Started says served those images to
|
|
||||||
anyone without a signature, at any size and in any format.
|
|
||||||
`configs/config.example.yml` now leaves `allowlist_hosts` out, with one
|
|
||||||
commented entry under `example.com`. Getting Started and "Running under upaas"
|
|
||||||
in `README.md` say that every image on an allowlisted host is served without a
|
|
||||||
signature, so an allowlisted host must be one the operator controls.
|
|
||||||
- 2026-10-15 the host of a `/v1/image/` URL is lower-cased, and one with a port
|
- 2026-10-15 the host of a `/v1/image/` URL is lower-cased, and one with a port
|
||||||
needs a signature (closes #246): with `cdn.example.com` allowlisted,
|
needs a signature (closes #246): with `cdn.example.com` allowlisted,
|
||||||
`CDN.Example.com` and `cdn.example.com:8443` passed the allowlist too, and
|
`CDN.Example.com` and `cdn.example.com:8443` passed the allowlist too, and
|
||||||
|
|||||||
@@ -45,14 +45,15 @@ state_dir: ./data
|
|||||||
signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32"
|
signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32"
|
||||||
|
|
||||||
# Hosts that don't require signatures (default: none)
|
# Hosts that don't require signatures (default: none)
|
||||||
# Every image on an allowlisted host is served to anyone without a
|
|
||||||
# signature, at any size and in any format, so list only hosts you
|
|
||||||
# control, where nobody else can put images.
|
|
||||||
# Use "." prefix for wildcard subdomain matching (e.g., ".example.com" matches "cdn.example.com")
|
# Use "." prefix for wildcard subdomain matching (e.g., ".example.com" matches "cdn.example.com")
|
||||||
# An entry that is neither a host name nor an IP address (IPv6 without
|
# An entry that is neither a host name nor an IP address (IPv6 without
|
||||||
# brackets), such as one with a port or a "*." wildcard, aborts startup.
|
# brackets), such as one with a port or a "*." wildcard, aborts startup.
|
||||||
# allowlist_hosts:
|
allowlist_hosts:
|
||||||
# - images.example.com
|
- s3.sneak.cloud
|
||||||
|
- static.sneak.cloud
|
||||||
|
- sneak.berlin
|
||||||
|
- github.com
|
||||||
|
- user-images.githubusercontent.com
|
||||||
|
|
||||||
# Hosts whose pages may not show pixa's images, written as for
|
# Hosts whose pages may not show pixa's images, written as for
|
||||||
# allowlist_hosts. A request to /v1/image/ or /v1/e/ whose Referer header
|
# allowlist_hosts. A request to /v1/image/ or /v1/e/ whose Referer header
|
||||||
|
|||||||
@@ -92,32 +92,6 @@ func TestGetStringSlice_Empty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExampleConfigAllowlistsNoHost loads configs/config.example.yml
|
|
||||||
// with a real signing key, as Getting Started in README.md has it copied
|
|
||||||
// and edited, and checks that it allowlists no host: every image on an
|
|
||||||
// allowlisted host is served to anyone without a signature.
|
|
||||||
func TestExampleConfigAllowlistsNoHost(t *testing.T) {
|
|
||||||
t.Setenv("PIXA_SIGNING_KEY", validTestSigningKey)
|
|
||||||
|
|
||||||
examplePath := filepath.Join("..", "..", "configs", "config.example.yml")
|
|
||||||
|
|
||||||
sc, err := loadTestConfig(examplePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to load %s: %v", examplePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := newFromSmartConfig(sc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s is not a valid config: %v", examplePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("allowlist_hosts in %s: %v", examplePath, c.AllowlistHosts)
|
|
||||||
|
|
||||||
if len(c.AllowlistHosts) != 0 {
|
|
||||||
t.Errorf("%s allowlists %v, want no host", examplePath, c.AllowlistHosts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadTestConfig is a helper to load a config file for testing.
|
// loadTestConfig is a helper to load a config file for testing.
|
||||||
func loadTestConfig(path string) (*smartconfig.Config, error) {
|
func loadTestConfig(path string) (*smartconfig.Config, error) {
|
||||||
return smartconfig.NewFromConfigPath(path)
|
return smartconfig.NewFromConfigPath(path)
|
||||||
|
|||||||
@@ -301,46 +301,6 @@ func TestService_ValidateRequest_HostWithPortNeedsSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestService_ValidateRequest_UnbracketedIPv6PortNeedsSignature verifies that
|
|
||||||
// an allowlisted IPv6 address written without brackets and followed by a
|
|
||||||
// port, or by a colon with no port, needs a signature. Go's URL parser, which
|
|
||||||
// the fetcher uses, reads 2001:db8::1:8443 as the host 2001:db8::1 with the
|
|
||||||
// port 8443.
|
|
||||||
func TestService_ValidateRequest_UnbracketedIPv6PortNeedsSignature(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
svc, _ := SetupTestService(t,
|
|
||||||
WithSigningKey(testSigningKey),
|
|
||||||
WithAllowlist("2001:db8::1"),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, host := range []string{"2001:db8::1:8443", "2001:db8::1:"} {
|
|
||||||
t.Run(host, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
req := &ImageRequest{
|
|
||||||
SourceHost: host,
|
|
||||||
SourcePath: testPathCat,
|
|
||||||
Size: Size{Width: 50, Height: 50},
|
|
||||||
Format: FormatJPEG,
|
|
||||||
Quality: 85,
|
|
||||||
FitMode: FitCover,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := svc.ValidateRequest(req)
|
|
||||||
t.Logf("ValidateRequest() for host %q with no signature: %v",
|
|
||||||
host, err)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("host %q served without a signature, want refused",
|
|
||||||
host)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestService_Get_InvalidFile(t *testing.T) {
|
func TestService_Get_InvalidFile(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user