Skip to content

Cloud IP Store

CloudIpStoreProtocol defines the pluggable backend for cached cloud-provider IP ranges. Guard-core ships two concrete implementations — InMemoryCloudIpStore and RedisCloudIpStore — and the active store can be swapped via SecurityConfig.cloud_ip_store.


CloudIpStoreProtocol

Location: guard_core/protocols/cloud_ip_store_protocol.py

from typing import Protocol, runtime_checkable


@runtime_checkable
class CloudIpStoreProtocol(Protocol):
    async def get(self, provider: str) -> set[str] | None: ...
    async def set(
        self, provider: str, ranges: set[str], ttl: int | None = None
    ) -> None: ...
    async def clear(self) -> None: ...
Method Description
get(provider) Return the cached CIDR set for the provider, or None if absent.
set(provider, ranges, ttl=) Cache the CIDR set for the provider. ttl is honored by stores that support it.
clear() Drop all cached entries.

A sync mirror lives at guard_core/sync/protocols/cloud_ip_store_protocol.py as SyncCloudIpStoreProtocol.


InMemoryCloudIpStore

Location: guard_core/handlers/cloud_ip_stores.py

class InMemoryCloudIpStore:
    def __init__(self) -> None: ...
    async def get(self, provider: str) -> set[str] | None: ...
    async def set(self, provider: str, ranges: set[str], ttl: int | None = None) -> None: ...
    async def clear(self) -> None: ...

Process-local dictionary backed store. The ttl argument is accepted for protocol compatibility but is not enforced. This is the default store when no Redis is configured.


RedisCloudIpStore

Location: guard_core/handlers/cloud_ip_stores.py

class RedisCloudIpStore:
    def __init__(
        self,
        redis_handler: RedisHandlerProtocol,
        key_prefix: str = "cloud_ip",
    ) -> None: ...
    async def get(self, provider: str) -> set[str] | None: ...
    async def set(self, provider: str, ranges: set[str], ttl: int | None = None) -> None: ...
    async def clear(self) -> None: ...

Redis-backed store. Each provider's CIDR set is JSON-encoded as a sorted list and written under <redis_prefix><key_prefix>:<provider> (the RedisManager.set_key path already prepends config.redis_prefix, so key_prefix should not duplicate it). set() honors the optional ttl. clear() removes every key under the resolved prefix.


Backends

Default (auto-constructed)

When enable_redis=True and block_cloud_providers is set, guard-core automatically wires a RedisCloudIpStore during HandlerInitializer.initialize_redis_handlers() so cloud-IP ranges persist across worker restarts and stay shared across replicas. No cloud_ip_store= setting is required.

from guard_core.models import SecurityConfig

config = SecurityConfig(
    enable_redis=True,
    redis_url="redis://localhost:6379",
    redis_prefix="myapp:guard:",
    block_cloud_providers={"AWS", "GCP", "Azure"},
)

Cloud-IP ranges land at Redis keys like myapp:guard:cloud_ip:AWS. The redundant guard: segment in the previous key_prefix default was removed in this release (see CHANGELOG); RedisManager.set_key already prepends redis_prefix, so the key_prefix no longer duplicates it.

When enable_redis=False (or no Redis URL is reachable), the same path falls back to InMemoryCloudIpStore — fine for single-process deployments, lost on restart.

Custom prefix or implementation via callable

cloud_ip_store accepts a CloudIpStoreFactory callable: Callable[[RedisHandlerProtocol], CloudIpStoreProtocol]. The handler initializer invokes it with the live Redis handler once Redis is up, so user code never has to construct a throwaway RedisManager purely to feed the store.

from guard_core.handlers.cloud_ip_stores import RedisCloudIpStore
from guard_core.models import SecurityConfig

config = SecurityConfig(
    enable_redis=True,
    redis_url="redis://localhost:6379",
    cloud_ip_store=lambda redis: RedisCloudIpStore(redis, key_prefix="cloud_ip_v2"),
)

Custom non-Redis implementation via instance

If the store does not need a Redis handle (in-memory, DynamoDB, SQL, etc.), pass an instance directly:

from guard_core.models import SecurityConfig

config = SecurityConfig(
    cloud_ip_store=MyDynamoDBCloudIpStore(table_name="cloud_ips"),
)

The instance is wired straight into cloud_handler.set_store(...) after Redis bootstrap. Combined with lazy_init=True (the default), the first cloud-IP fetch happens in a background task — application startup does not block on it.


Redis namespace migration

The v2.0.0 release moved the cloud-IP cache from the legacy cloud_ranges namespace (comma-separated CSV values per provider) to cloud_ip (JSON-encoded sorted list per provider, prefixed by redis_prefix).

  • Default writersRedisCloudIpStore writes to <redis_prefix>cloud_ip:<provider>.
  • Legacy reader (dead code) — the legacy CSV path under cloud_ranges is gated behind CloudManager._store is None. Because CloudManager.__new__ always seeds _store with an InMemoryCloudIpStore() and nothing resets it to None, that branch is unreachable at runtime; the default and the Redis store both use the new cloud_ip namespace.

Any ops tooling, dashboards, or sidecars reading those Redis keys directly must switch to the new namespace. On upgrade from a previous release, the cache invalidates once and repopulates within cloud_ip_refresh_interval.


See also