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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 91 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
```

The `insecure-bidders` cargo feature on `auctioneer-config-data` enables
deterministic loopback test bidders:
The `insecure-bidders` Cargo feature permits development HTTP/loopback
transport. Mocktioneer itself is available in normal builds:

```
cargo test --workspace --features auctioneer-config-data/insecure-bidders
cargo test --workspace --features insecure-bidders
```

Axum's default `cargo test -p auctioneer-adapter-axum --all-targets` also runs the
handler-to-socket privacy test. Its test-only dependency enables the loopback
bidder; normal production builds retain the secure defaults. The test sweeps
handler-to-socket privacy test. Its test-only dependency permits loopback
transport; the fixture explicitly enables and configures its bidder. The test sweeps
CCPA and all three disclosure activities independently.

## Auction privacy configuration
Expand Down Expand Up @@ -230,15 +230,96 @@ Run them from the workspace root.
| Fastly Compute@Edge | `fastly compute serve` |
| Spin | `spin up --from crates/auctioneer-adapter-spin` |

### Configure a Mocktioneer destination

Mocktioneer is available in normal builds and is disabled by default. Enable
it and configure its HTTPS destination in your runtime policy:

```toml
[bidders.mocktioneer]
enabled = true
implementation = "mocktioneer"

[bidders.mocktioneer.settings]
endpoint = "https://mock-bidder.example.test/openrtb2/auction"
```

When settings are supplied, the bidder row requires `implementation`; the
separate `.settings` subconfig contains only that owner's fields. The key must
match the resolved bidder owner. Omit both fields to use inherited or compiled
defaults. Republish older wrapped configuration in this form after updating
the binary.

Use a literal absolute URL of at most 2048 UTF-8 bytes. Public destinations
require HTTPS; the development feature also allows HTTP to exact loopback
literals. Private/reserved addresses, localhost-alias services, credentials,
fragments and templates are rejected. Use ASCII host names (punycode for
internationalized names) and percent-encode characters the HTTP transport
cannot represent. Omitted/null endpoint values inherit the base bidder setting.
An enabled Mocktioneer requires a resolved endpoint; it has no compiled default.
An empty endpoint is invalid.
Impression parameters cannot override the selected destination.

Validate and push the complete policy to local Fastly/Viceroy from the workspace
root, then restart Viceroy to read the updated local config store:

```sh
auctioneer-cli config validate --app-config auctioneer.fastly-local.toml --no-env
auctioneer-cli config push --adapter fastly --local --app-config auctioneer.fastly-local.toml --no-env
```

HTTPS needs no extra Cargo features in either the CLI or the serving guest.
For an explicitly configured local endpoint such as
`http://127.0.0.1:8787/openrtb2/auction`, build the CLI with
`--features auctioneer-core/insecure-bidders` and the serving adapter with
`--features insecure-bidders`. The feature never supplies a destination.
If another configured implementation is missing from the build, CLI errors and runtime
operator logs identify its compiled name and required Cargo feature. These
diagnostics omit configured alias names, endpoint values and credentials.
Endpoint changes take effect through the existing config publication lifecycle;
they require no endpoint-specific rebuild. The other bidders' compiled endpoint
and host-allowlist rules remain unchanged.

## Operational endpoints (`/_auctioneer/*`)

`GET /_auctioneer/health/gvl` reports GVL archive health (archive age,
bundled vendor-list versions, and the rolling unavailable-lookup
counter). The handler is fail-closed: it answers 401 unless the adapter
boot installed an explicit access policy — either a bearer token
(`AUCTIONEER__RUNTIME__HEALTH_GVL_TOKEN` on Axum / Cloudflare; the platform
secret store on Fastly / Spin) or the machine-checkable
`allow_unauthenticated_health = true` opt-in.
counter). Configure access in the pushed operator policy:

```toml
[host.gvl]
authenticate_health = true
authenticate_health_token_secret = "health_gvl_token"
```

The handler is fail-closed. `authenticate_health_token_secret` names an entry in
the declared default Secret Store. Omit the section or `authenticate_health` to
keep health closed. `authenticate_health = true` requires a nonempty reference;
an empty reference is invalid. To permit anonymous access, set
`authenticate_health = false` and omit `authenticate_health_token_secret`;
an empty or named reference alongside false is invalid. The handler never reads
a secret in anonymous mode. Authenticated references must be at most 128
UTF-8 bytes, without ASCII controls or surrounding whitespace. A reference without
a mode, unknown fields and explicit nulls are rejected. The shared handler resolves
config and, for authenticated health, the selected secret on each health request;
updates take effect with the provider's existing
config/secret visibility lifecycle. Axum's file-backed config keeps its startup
snapshot until restart. Missing or invalid policy returns 401 with a Bearer
challenge. In authenticated mode, unavailable/empty secrets and secrets larger
than 4096 bytes also return 401. `/status`
remains independent. Ordinary routes do not fetch health credentials.

Provision `health_gvl_token` through the existing SDK secret binding:

- Axum: `EDGEZERO__SECRETS__HEALTH_GVL_TOKEN`; entry names accept ASCII letters,
digits and underscores and are uppercased for this environment lookup.
- Cloudflare: `wrangler secret put health_gvl_token` uses the entry name literally.
- Fastly: the `health_gvl_token` item in the linked default secret-store alias.
The local manifest's `AUCTIONEER__RUNTIME__HEALTH_GVL_TOKEN` is only a Viceroy
provisioning source; the runtime does not read that environment variable.
- Spin: a secret application variable declared and mapped into the component.
The SDK lowercases entry names; a different `authenticate_health_token_secret`
needs its own matching declaration and component mapping.

Deployments that opt in to serving `/_auctioneer/*` without a token MUST
restrict the `/_auctioneer/*` path prefix at the ingress layer. That
Expand Down
32 changes: 24 additions & 8 deletions auctioneer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ schema_version = 1
# Plain-text response used by the status handler.
status_response = "auctioneer local"

# Omit this section or authenticate_health to keep GVL health closed. The secret
# reference names an entry in the declared default Secret Store, never token bytes.
# Nonempty names are 1..128 UTF-8 bytes, without ASCII controls or surrounding
# whitespace. authenticate_health = true requires a nonempty reference.
# To permit anonymous health, set authenticate_health to false and omit the
# secret reference; even an empty reference alongside false is invalid.
# Anonymous health never reads a secret; restrict /_auctioneer/* at ingress.
# A reference without authenticate_health, unknown fields and explicit nulls fail.
[host.gvl]
authenticate_health = true
authenticate_health_token_secret = "health_gvl_token"

# Requested limits can lower, but never expand, independently installed capacity.
# Durations are milliseconds, sizes are bytes, and counts are integers.
[host.resource_limits]
Expand Down Expand Up @@ -278,37 +290,41 @@ returnallbidstatus_default = false
# per field through the observable name, base, selected account and host, and
# remains bounded by installed capacity. timeout_ms is 1..30000; sample_rate is
# finite 0.0..1.0. enabled=false disables with a warning; sample_rate=0 silently
# excludes traffic. Settings do not register a new implementation or endpoint.
# excludes traffic. Settings do not register or activate a new implementation.
#
# This block is a FIELD REFERENCE, not a deployable combined settings object.
# Replace fake_bidder with a compiled public name, implementation with its owner,
# and keep ONLY values declared by that owner's bidder.toml [settings.fields].
# The values table lists every currently supported bidder-owned field across all
# implementations. Do not paste that union into a single bidder's settings.
# Replace fake_bidder with a compiled public name, set implementation to its
# owner, and keep ONLY fields declared by that owner's bidder.toml
# [settings.fields]. The settings table lists every currently supported
# bidder-owned field across all implementations. Do not paste that union into a
# single bidder's settings.
# implementation: required owner key on the bidder row whenever settings are
# supplied. It must match the resolved bidder owner. The settings subconfig holds
# only that owner's fields. Omit both implementation and settings to use defaults.
# hb_source/hb_source_video: integer protocol source values.
# supply_id: nonempty routing identifier when required by the implementation.
# credentials_secret: a secret-store key, never the credential bundle itself.
# external_url: publisher-facing URL used by the owning protocol.
# region: "us-east", "us-west", "eu" or "apac" for the regional implementation.
# endpoint: required operator destination for enabled Mocktioneer; HTTPS works in normal builds.
# BEGIN BIDDER EXAMPLE
# [bidders.fake_bidder]
# enabled = true
# implementation = "fake_implementation"
# usersync_enabled = false
# timeout_ms = 300
# sample_rate = 1.0
# max_transport_bytes = 131072
# max_decoded_bytes = 262144
#
# [bidders.fake_bidder.settings]
# implementation = "fake_implementation"
#
# [bidders.fake_bidder.settings.values]
# hb_source = 5
# hb_source_video = 6
# supply_id = "fake_supply"
# credentials_secret = "fake_credential_bundle"
# external_url = "https://publisher.example"
# region = "us-east"
# endpoint = "https://mock-bidder.example.test/openrtb2/auction"
# END BIDDER EXAMPLE

# Optional floor-only currency conversion. Append this block to opt in; response
Expand Down
13 changes: 3 additions & 10 deletions crates/auctioneer-adapter-axum/src/baseline_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
//! It also removes the reading's dependence on page-cache state, which
//! is what makes repeated runs comparable rather than a lottery.

use std::env;
use std::hint::black_box;
use std::io::{self, Write as _};
use std::sync::Arc;
Expand All @@ -40,15 +39,12 @@ use std::time::Duration;

use auctioneer_adapter_axum::{envelope, timer::TokioTimer};
use auctioneer_core::host_primitives::{
BearerToken, DevRng, SystemClock, axum_production_strategy, bootstrap_adapter_with_envelope,
decide_health_policy, install_health_primitives, read_bool_flag,
DevRng, SystemClock, axum_production_strategy, bootstrap_adapter_with_envelope,
install_health_counter_scope,
};
use auctioneer_runtime::NoopDecodeCooperator;
use edgezero_core::env_config::EnvConfig;

const HEALTH_TOKEN_VAR: &str = "AUCTIONEER__RUNTIME__HEALTH_GVL_TOKEN";
const ALLOW_UNAUTH_VAR: &str = "AUCTIONEER__RUNTIME__ALLOW_UNAUTHENTICATED_HEALTH";

/// This host's page size. The touch stride only has to be no larger
/// than the real page size to reach every page; 4 KiB is the
/// measurement contract's stated stride and is a divisor of this host's
Expand Down Expand Up @@ -133,10 +129,7 @@ fn main() -> anyhow::Result<()> {
)
.map_err(|error| anyhow::anyhow!("boot envelope rejected: {error}"))?;

let raw_flag = env::var(ALLOW_UNAUTH_VAR).ok();
let allow_unauth = read_bool_flag(ALLOW_UNAUTH_VAR, raw_flag.as_deref())?;
let token = env::var(HEALTH_TOKEN_VAR).ok().and_then(BearerToken::new);
install_health_primitives(decide_health_policy(token, allow_unauth)?, "process")?;
install_health_counter_scope("process")?;

let walked = touch_embedded_archive();

Expand Down
23 changes: 12 additions & 11 deletions crates/auctioneer-adapter-axum/src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,8 +733,9 @@ mod tests {
.unwrap();
assert_eq!(
derived.total_bytes,
// Two admitted auctions each retain the measured bridge owners.
2_536_754_680 + 2 * 128 * 1024 + 26_214_400 + 2 * 19_160
// Two admitted auctions each retain the prior 19,160-byte bridge
// owners plus 96 measured bytes for the GVL policy execution frame.
2_536_754_680 + 2 * 128 * 1024 + 26_214_400 + 2 * (19_160 + 96)
);
assert!(derived.total_bytes + (256_u64 << 20_u32) <= row.isolate_total_bytes);
let mut underfunded = row;
Expand Down Expand Up @@ -811,11 +812,11 @@ mod tests {
// One sequential credential bank per admitted auction, not per call.
assert_eq!(
previous.total_bytes,
2_355_338_744 + 2 * (1_576_960 + 128 * 1024) + 26_214_400 + 2 * 19_160
2_355_338_744 + 2 * (1_576_960 + 128 * 1024) + 26_214_400 + 2 * (19_160 + 96)
);
assert_eq!(
previous.per_auction_peak,
565_731_420 + 1_576_960 + 128 * 1024 + 0x0008_0000 + 19_160
565_731_420 + 1_576_960 + 128 * 1024 + 0x0008_0000 + (19_160 + 96)
);
assert_eq!(previous.bookkeeping_term, 917_504);
assert_eq!(
Expand Down Expand Up @@ -891,16 +892,16 @@ mod tests {
assert_eq!(row.isolate_total_bytes, 4_u64 << 30_u32);
assert_eq!(
derived.total_bytes,
2_536_754_680 + 2 * 128 * 1024 + 26_214_400 + 2 * 19_160
2_536_754_680 + 2 * 128 * 1024 + 26_214_400 + 2 * (19_160 + 96)
);
assert_eq!(
derived.per_auction_peak,
654_862_428 + 1_576_960 + 128 * 1024 + 0x0008_0000 + 19_160
654_862_428 + 1_576_960 + 128 * 1024 + 0x0008_0000 + (19_160 + 96)
);
assert_eq!(derived.bookkeeping_term, 3_276_800);
assert_eq!(
derived.margin_bytes,
1_761_366_536 - 2 * (1_576_960 + 128 * 1024) - 26_214_400 - 2 * 19_160
1_761_366_536 - 2 * (1_576_960 + 128 * 1024) - 26_214_400 - 2 * (19_160 + 96)
);
assert_eq!(
derived.total_bytes,
Expand Down Expand Up @@ -1278,7 +1279,7 @@ mod tests {
);
assert_eq!(
derived.total_bytes,
2_536_384_248 + 2 * 128 * 1024 + 26_214_400 + 2 * 19_160
2_536_384_248 + 2 * 128 * 1024 + 26_214_400 + 2 * (19_160 + 96)
);
assert_eq!(
derived.margin_bytes,
Expand All @@ -1287,7 +1288,7 @@ mod tests {
assert_eq!(derived.auction_charges, expected_charges(&roomy));
assert_eq!(
derived.auction_charges.privacy,
(14_u64 << 20_u32) + 800 + 32 + 19_160
(14_u64 << 20_u32) + 800 + 32 + (19_160 + 96)
);
assert_eq!(derived.runtime_config_term, 30_400_512);
assert_eq!(
Expand All @@ -1310,7 +1311,7 @@ mod tests {
assert_eq!(derived.debug_term, 9_310_368);
assert_eq!(
derived.per_auction_peak,
654_677_212 + 1_576_960 + 128 * 1024 + 0x0008_0000 + 19_160
654_677_212 + 1_576_960 + 128 * 1024 + 0x0008_0000 + (19_160 + 96)
);
assert_eq!(
u64::from(limits().max_outbound_requests)
Expand All @@ -1321,7 +1322,7 @@ mod tests {
);
assert_eq!(
u64::from(limits().max_concurrent_auctions) * derived.per_auction_peak,
1_309_354_424 + 2 * (1_576_960 + 128 * 1024 + 0x0008_0000 + 19_160),
1_309_354_424 + 2 * (1_576_960 + 128 * 1024 + 0x0008_0000 + (19_160 + 96)),
);
// The response node charge is independent of the privacy enforcement byte charge.
assert_eq!(
Expand Down
2 changes: 1 addition & 1 deletion crates/auctioneer-adapter-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use auctioneer_config_data::types::BidderInfo;
pub fn build_features() -> Vec<&'static str> {
let endpoint = "http://127.0.0.1:8999";
let info = BidderInfo {
endpoint: endpoint.to_owned(),
endpoint: Some(endpoint.to_owned()),
..BidderInfo::default()
};
let mut features = if auctioneer_exchange::validate_endpoint(endpoint, &info).is_ok() {
Expand Down
Loading
Loading