feat: improve WireGuard client management and framework initialization

- Improve WireGuard client IP calculation logic (find next available IP)
- Add local wireguard-clients directory for storing client configs
- Integrate Redis pool into CacheInitializer
- Improve ContainerBootstrapper with better imports and Redis pool
- Add monitoring role tags for better task organization
- Update WireGuard documentation
- Store generated WireGuard client configs locally
This commit is contained in:
2025-11-02 03:29:23 +01:00
parent f56d53d873
commit e598309c48
12 changed files with 183 additions and 37 deletions

View File

@@ -9,6 +9,7 @@
wireguard_config_path: "/etc/wireguard" wireguard_config_path: "/etc/wireguard"
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf" wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
wireguard_client_configs_path: "/etc/wireguard/clients" wireguard_client_configs_path: "/etc/wireguard/clients"
wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients"
pre_tasks: pre_tasks:
- name: Set WireGuard network - name: Set WireGuard network
@@ -60,14 +61,28 @@
set_fact: set_fact:
server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}" server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}"
- name: Count existing clients in config - name: Extract WireGuard server IP octets
set_fact: set_fact:
existing_clients_count: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('\\[Peer\\]') | length) }}" wireguard_server_ip_octets: "{{ server_vpn_ip.split('.') }}"
when: client_ip == ""
- name: Gather existing client addresses
set_fact:
existing_client_ips: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('AllowedIPs = ([0-9A-Za-z.]+)/32', '\\1')) }}"
when: client_ip == "" when: client_ip == ""
- name: Calculate client IP if not provided - name: Calculate client IP if not provided
vars:
existing_last_octets: "{{ (existing_client_ips | default([])) | map('regex_replace', '^(?:\\d+\\.\\d+\\.\\d+\\.)', '') | select('match', '^[0-9]+$') | map('int') | list }}"
server_last_octet: "{{ wireguard_server_ip_octets[3] | int }}"
next_octet_candidate: "{{ (existing_last_octets + [server_last_octet]) | map('int') | list | max + 1 if (existing_last_octets + [server_last_octet]) else server_last_octet + 1 }}"
set_fact: set_fact:
client_ip: "{{ server_vpn_ip | regex_replace('^(\\d+\\.\\d+\\.\\d+\\.)\\d+$', '\\1') }}{{ (server_vpn_ip | regex_replace('^(\\d+\\.\\d+\\.\\d+\\.)(\\d+)', '\\2') | int) + (existing_clients_count | default(1)) | int }}" client_ip: "{{ [
wireguard_server_ip_octets[0],
wireguard_server_ip_octets[1],
wireguard_server_ip_octets[2],
next_octet_candidate
] | join('.') }}"
when: client_ip == "" when: client_ip == ""
- name: Generate client private key - name: Generate client private key
@@ -84,12 +99,6 @@
changed_when: false changed_when: false
no_log: yes no_log: yes
- name: Check if client already exists in config
shell: "grep -q '{{ client_name }}' {{ wireguard_config_file }} || echo 'not found'"
register: client_exists_check
changed_when: false
failed_when: false
- name: Add client to WireGuard server config - name: Add client to WireGuard server config
blockinfile: blockinfile:
path: "{{ wireguard_config_file }}" path: "{{ wireguard_config_file }}"
@@ -99,7 +108,7 @@
PublicKey = {{ client_public_key.stdout }} PublicKey = {{ client_public_key.stdout }}
AllowedIPs = {{ client_ip }}/32 AllowedIPs = {{ client_ip }}/32
marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}" marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}"
when: "'not found' in client_exists_check.stdout" register: wireguard_client_block
- name: Ensure client configs directory exists - name: Ensure client configs directory exists
file: file:
@@ -109,6 +118,15 @@
owner: root owner: root
group: root group: root
- name: Ensure local client configs directory exists
file:
path: "{{ wireguard_local_client_configs_dir }}"
state: directory
mode: '0700'
delegate_to: localhost
become: no
run_once: true
- name: Get server public key - name: Get server public key
shell: "cat {{ wireguard_config_path }}/{{ wireguard_interface }}_private.key | wg pubkey" shell: "cat {{ wireguard_config_path }}/{{ wireguard_interface }}_private.key | wg pubkey"
register: server_public_key_cmd register: server_public_key_cmd
@@ -124,6 +142,20 @@
owner: root owner: root
group: root group: root
- name: Download client configuration to control machine
fetch:
src: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
dest: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf"
flat: yes
mode: '0600'
- name: Ensure local client configuration has strict permissions
file:
path: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf"
mode: '0600'
delegate_to: localhost
become: no
- name: Read WireGuard server config to find server IP - name: Read WireGuard server config to find server IP
slurp: slurp:
src: "{{ wireguard_config_file }}" src: "{{ wireguard_config_file }}"
@@ -133,7 +165,7 @@
systemd: systemd:
name: "wg-quick@{{ wireguard_interface }}" name: "wg-quick@{{ wireguard_interface }}"
state: restarted state: restarted
when: "'not found' in client_exists_check.stdout" when: wireguard_client_block.changed
- name: Display client configuration - name: Display client configuration
debug: debug:
@@ -145,6 +177,9 @@
Client Configuration File: Client Configuration File:
{{ wireguard_client_configs_path }}/{{ client_name }}.conf {{ wireguard_client_configs_path }}/{{ client_name }}.conf
Local Copy:
{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf
Client IP: {{ client_ip }} Client IP: {{ client_ip }}
Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }} Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }}

View File

@@ -3,3 +3,4 @@ monitoring_stack_path: "{{ stacks_base_path }}/monitoring"
monitoring_wait_timeout: "{{ wait_timeout | default(60) }}" monitoring_wait_timeout: "{{ wait_timeout | default(60) }}"
monitoring_env_template: "{{ role_path }}/../../templates/monitoring.env.j2" monitoring_env_template: "{{ role_path }}/../../templates/monitoring.env.j2"
monitoring_vault_file: "{{ role_path }}/../../secrets/production.vault.yml" monitoring_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
monitoring_vpn_ip_whitelist: "{{ wireguard_network_default | default('10.8.0.0/24') }}"

View File

@@ -5,6 +5,8 @@
delegate_to: localhost delegate_to: localhost
register: monitoring_vault_stat register: monitoring_vault_stat
become: no become: no
tags:
- monitoring
- name: Optionally load monitoring secrets from vault - name: Optionally load monitoring secrets from vault
include_vars: include_vars:
@@ -13,16 +15,22 @@
no_log: yes no_log: yes
delegate_to: localhost delegate_to: localhost
become: no become: no
tags:
- monitoring
- name: Set Grafana admin password from vault or generate - name: Set Grafana admin password from vault or generate
set_fact: set_fact:
grafana_admin_password: "{{ vault_grafana_admin_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}" grafana_admin_password: "{{ vault_grafana_admin_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}"
no_log: yes no_log: yes
tags:
- monitoring
- name: Set Prometheus password from vault or generate - name: Set Prometheus password from vault or generate
set_fact: set_fact:
prometheus_password: "{{ vault_prometheus_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}" prometheus_password: "{{ vault_prometheus_password | default(lookup('password', '/dev/null length=25 chars=ascii_letters,digits')) }}"
no_log: yes no_log: yes
tags:
- monitoring
- name: Generate Prometheus BasicAuth hash - name: Generate Prometheus BasicAuth hash
shell: | shell: |
@@ -30,17 +38,23 @@
register: prometheus_auth_hash register: prometheus_auth_hash
changed_when: false changed_when: false
no_log: yes no_log: yes
tags:
- monitoring
- name: Set Prometheus BasicAuth string - name: Set Prometheus BasicAuth string
set_fact: set_fact:
prometheus_auth: "admin:{{ prometheus_auth_hash.stdout }}" prometheus_auth: "admin:{{ prometheus_auth_hash.stdout }}"
no_log: yes no_log: yes
tags:
- monitoring
- name: Ensure monitoring stack directory exists - name: Ensure monitoring stack directory exists
file: file:
path: "{{ monitoring_stack_path }}" path: "{{ monitoring_stack_path }}"
state: directory state: directory
mode: '0755' mode: '0755'
tags:
- monitoring
- name: Create monitoring stack .env file - name: Create monitoring stack .env file
template: template:
@@ -50,6 +64,8 @@
group: "{{ ansible_user }}" group: "{{ ansible_user }}"
mode: '0600' mode: '0600'
no_log: yes no_log: yes
tags:
- monitoring
- name: Deploy Monitoring stack - name: Deploy Monitoring stack
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
@@ -57,12 +73,18 @@
state: present state: present
pull: always pull: always
register: monitoring_compose_result register: monitoring_compose_result
tags:
- monitoring
- name: Wait for Monitoring to be ready - name: Wait for Monitoring to be ready
wait_for: wait_for:
timeout: "{{ monitoring_wait_timeout }}" timeout: "{{ monitoring_wait_timeout }}"
when: monitoring_compose_result.changed when: monitoring_compose_result.changed
tags:
- monitoring
- name: Record monitoring deployment facts - name: Record monitoring deployment facts
set_fact: set_fact:
monitoring_stack_changed: "{{ monitoring_compose_result.changed | default(false) }}" monitoring_stack_changed: "{{ monitoring_compose_result.changed | default(false) }}"
tags:
- monitoring

View File

@@ -4,6 +4,9 @@
# Domain Configuration # Domain Configuration
DOMAIN={{ app_domain }} DOMAIN={{ app_domain }}
# VPN Access Control
MONITORING_VPN_IP_WHITELIST={{ monitoring_vpn_ip_whitelist }}
# Grafana Configuration # Grafana Configuration
GRAFANA_ADMIN_USER={{ grafana_admin_user | default('admin') }} GRAFANA_ADMIN_USER={{ grafana_admin_user | default('admin') }}
GRAFANA_ADMIN_PASSWORD={{ grafana_admin_password }} GRAFANA_ADMIN_PASSWORD={{ grafana_admin_password }}

View File

@@ -0,0 +1,27 @@
# WireGuard Client Configuration for grafana-test
# Generated by Ansible - DO NOT EDIT MANUALLY
[Interface]
# Client private key
PrivateKey = uMhNKh+Wi0aykTnazfSJD6l7Wc2V1Pe+7rFtFcnfynw=
# Client IP address in VPN network
Address = 10.8.0.4/24
# DNS server (optional)
DNS = 1.1.1.1, 8.8.8.8
[Peer]
# Server public key
PublicKey = hT3OCWZ6ElX79YdAdexSsZnbWLzRM/5szk+XNEBUaS8=
# Server endpoint
Endpoint = 94.16.110.151:51820
# Allowed IPs (routes through VPN)
# IMPORTANT: Only VPN network is routed through VPN by default
# SSH access via normal IP (94.16.110.151) remains available
AllowedIPs = 10.8.0.0/24
# Keep connection alive
PersistentKeepalive = 25

View File

@@ -0,0 +1,27 @@
# WireGuard Client Configuration for mikepc
# Generated by Ansible - DO NOT EDIT MANUALLY
[Interface]
# Client private key
PrivateKey = wFxqFHe4R8IVzkAQSHaAwVfwQ2rfm5vCySZMpvPsVUQ=
# Client IP address in VPN network
Address = 10.8.0.3/24
# DNS server (optional)
DNS = 1.1.1.1, 8.8.8.8
[Peer]
# Server public key
PublicKey = hT3OCWZ6ElX79YdAdexSsZnbWLzRM/5szk+XNEBUaS8=
# Server endpoint
Endpoint = 94.16.110.151:51820
# Allowed IPs (routes through VPN)
# IMPORTANT: Only VPN network is routed through VPN by default
# SSH access via normal IP (94.16.110.151) remains available
AllowedIPs = 10.8.0.0/24
# Keep connection alive
PersistentKeepalive = 25

View File

@@ -75,6 +75,8 @@ services:
- "traefik.http.routers.grafana.entrypoints=websecure" - "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls=true" - "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt" - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.grafana-vpn-only.ipwhitelist.sourcerange=${MONITORING_VPN_IP_WHITELIST:-10.8.0.0/24}"
- "traefik.http.routers.grafana.middlewares=grafana-vpn-only"
- "traefik.http.services.grafana.loadbalancer.server.port=3000" - "traefik.http.services.grafana.loadbalancer.server.port=3000"
depends_on: depends_on:
prometheus: prometheus:

View File

@@ -38,6 +38,24 @@ Folgende Dienste sollen später nur noch über VPN erreichbar sein:
- Aktuell: `https://portainer.michaelschiemer.de` (öffentlich) - Aktuell: `https://portainer.michaelschiemer.de` (öffentlich)
- Zukünftig: Nur `https://10.8.0.1:9443` über VPN - Zukünftig: Nur `https://10.8.0.1:9443` über VPN
## Aktueller Test: Grafana nur per VPN
- Realisiert über Traefik-IP-Whitelist (`grafana-vpn-only` Middleware)
- Whitelist-Wert: `MONITORING_VPN_IP_WHITELIST` in `deployment/stacks/monitoring/.env` (Ansible default `monitoring_vpn_ip_whitelist`)
- Ziel: Erst Grafana absichern, später Prometheus/Portainer nachziehen
### Rollout-Schritte
1. Neue Stacks auf den Server syncen: \
`ansible-playbook -i inventory/production.yml playbooks/sync-stacks.yml`
2. Monitoring-Rolle komplett laufen lassen (die `.env` muss generiert werden): \
`ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml --tags monitoring`
3. Prüfen, dass `monitoring_stack_changed` in der Zusammenfassung `true` ist oder der Grafana-Container neu gestartet wurde
### Verifikation
- Ohne VPN (öffentliche IP): `curl -I https://grafana.michaelschiemer.de``403 Forbidden`
- Mit WireGuard (`10.8.0.x` Client): Grafana im Browser laden, Login funktioniert
- Optional: Traefik-Logs prüfen (`docker compose -f ~/deployment/stacks/traefik/docker-compose.yml logs -f traefik`) für geblockte IPs
--- ---
## Geplante Implementierungsschritte ## Geplante Implementierungsschritte

View File

@@ -488,6 +488,10 @@ ansible-playbook -i inventory/production.yml playbooks/add-wireguard-client.yml
**Dokumentation**: Siehe `deployment/ansible/playbooks/README-WIREGUARD.md` **Dokumentation**: Siehe `deployment/ansible/playbooks/README-WIREGUARD.md`
> Nach dem Lauf liegt die Client-Konfiguration zusätzlich lokal im Repository unter
> `deployment/ansible/wireguard-clients/<client_name>.conf` ideal zum direkten
> Import auf deinem Admin-Rechner (Datei bleibt mit `chmod 600` geschützt).
--- ---
## Verzeichnisstruktur ## Verzeichnisstruktur

View File

@@ -19,6 +19,7 @@ use App\Framework\DI\Initializer;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface; use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Redis\RedisConfig; use App\Framework\Redis\RedisConfig;
use App\Framework\Redis\RedisConnection; use App\Framework\Redis\RedisConnection;
use App\Framework\Redis\RedisConnectionPool;
use App\Framework\Serializer\Json\JsonSerializer; use App\Framework\Serializer\Json\JsonSerializer;
use App\Framework\Serializer\Php\PhpSerializer; use App\Framework\Serializer\Php\PhpSerializer;
@@ -27,10 +28,9 @@ final readonly class CacheInitializer
public function __construct( public function __construct(
private PerformanceCollectorInterface $performanceCollector, private PerformanceCollectorInterface $performanceCollector,
private Container $container, private Container $container,
private ?AsyncService $asyncService = null,
#private CacheMetricsInterface $cacheMetrics, #private CacheMetricsInterface $cacheMetrics,
private string $redisHost = 'redis', private RedisConnectionPool $redisConnectionPool,
private int $redisPort = 6379, private ?AsyncService $asyncService = null,
private int $compressionLevel = -1, private int $compressionLevel = -1,
private int $minCompressionLength = 1024, private int $minCompressionLength = 1024,
private bool $enableAsync = true private bool $enableAsync = true
@@ -61,12 +61,8 @@ final readonly class CacheInitializer
throw new \RuntimeException('Redis extension is not loaded. Please install php-redis extension or use alternative cache drivers.'); throw new \RuntimeException('Redis extension is not loaded. Please install php-redis extension or use alternative cache drivers.');
} }
$redisConfig = new RedisConfig( $redisConnection = $this->redisConnectionPool->getCacheConnection();
host: $this->redisHost, #$redisConnection = new RedisConnection($redisConfig, 'cache');
port: $this->redisPort,
database: 1 // Use DB 1 for cache
);
$redisConnection = new RedisConnection($redisConfig, 'cache');
$redisCache = new GeneralCache(new RedisCache($redisConnection), $serializer, $compression); $redisCache = new GeneralCache(new RedisCache($redisConnection), $serializer, $compression);
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Fallback to file cache if Redis is not available // Fallback to file cache if Redis is not available

View File

@@ -13,12 +13,21 @@ use App\Framework\DI\ContainerCompiler;
use App\Framework\DI\DefaultContainer; use App\Framework\DI\DefaultContainer;
use App\Framework\DI\DependencyResolver; use App\Framework\DI\DependencyResolver;
use App\Framework\Discovery\DiscoveryServiceBootstrapper; use App\Framework\Discovery\DiscoveryServiceBootstrapper;
use App\Framework\Http\Parser\HttpRequestParser;
use App\Framework\Http\Parser\ParserCache;
use App\Framework\Http\Request;
use App\Framework\Http\RequestFactory;
use App\Framework\Http\ResponseEmitter; use App\Framework\Http\ResponseEmitter;
use App\Framework\Logging\DefaultLogger; use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\Formatter\DevelopmentFormatter;
use App\Framework\Logging\Handlers\ConsoleHandler;
use App\Framework\Logging\Logger; use App\Framework\Logging\Logger;
use App\Framework\Logging\LoggerInitializer; use App\Framework\Logging\LoggerInitializer;
use App\Framework\Logging\LogLevel;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface; use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Reflection\CachedReflectionProvider; use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Redis\RedisPoolInitializer;
use App\Framework\Config\Environment;
final readonly class ContainerBootstrapper final readonly class ContainerBootstrapper
{ {
@@ -149,19 +158,20 @@ final readonly class ContainerBootstrapper
$container->instance(Logger::class, $logger); $container->instance(Logger::class, $logger);
$container->instance(PerformanceCollectorInterface::class, $collector); $container->instance(PerformanceCollectorInterface::class, $collector);
$container->instance(Cache::class, new CacheInitializer($collector, $container)()); $pool = new RedisPoolInitializer($container, $container->get(Environment::class))->initialize();
$container->instance(Cache::class, new CacheInitializer($collector, $container, $pool)());
$container->instance(ResponseEmitter::class, new ResponseEmitter()); $container->instance(ResponseEmitter::class, new ResponseEmitter());
// TEMPORARY FIX: Manual RequestFactory binding until Discovery issue is resolved // TEMPORARY FIX: Manual RequestFactory binding until Discovery issue is resolved
$container->singleton(\App\Framework\Http\Request::class, function ($container) { $container->singleton(Request::class, function ($container) {
error_log("ContainerBootstrapper: Creating Request singleton"); error_log("ContainerBootstrapper: Creating Request singleton");
// Get Cache from container (it was just registered above) // Get Cache from container (it was just registered above)
$frameworkCache = $container->get(\App\Framework\Cache\Cache::class); $frameworkCache = $container->get(Cache::class);
$parserCache = new \App\Framework\Http\Parser\ParserCache($frameworkCache); $parserCache = new ParserCache($frameworkCache);
$parser = new \App\Framework\Http\Parser\HttpRequestParser($parserCache); $parser = new HttpRequestParser($parserCache);
$factory = new \App\Framework\Http\RequestFactory($parser); $factory = new RequestFactory($parser);
error_log("ContainerBootstrapper: About to call factory->createFromGlobals()"); error_log("ContainerBootstrapper: About to call factory->createFromGlobals()");
$request = $factory->createFromGlobals(); $request = $factory->createFromGlobals();
@@ -232,15 +242,15 @@ final readonly class ContainerBootstrapper
$handlers = $isMcpMode $handlers = $isMcpMode
? [new \App\Framework\Logging\Handlers\NullHandler()] ? [new \App\Framework\Logging\Handlers\NullHandler()]
: [ : [
new \App\Framework\Logging\Handlers\ConsoleHandler( new ConsoleHandler(
new \App\Framework\Logging\Formatter\DevelopmentFormatter(), new DevelopmentFormatter(),
\App\Framework\Logging\LogLevel::DEBUG LogLevel::DEBUG
) )
]; ];
$logger = new \App\Framework\Logging\DefaultLogger( $logger = new \App\Framework\Logging\DefaultLogger(
clock: $clock, clock: $clock,
minLevel: \App\Framework\Logging\LogLevel::DEBUG, minLevel: LogLevel::DEBUG,
handlers: $handlers, handlers: $handlers,
processorManager: new \App\Framework\Logging\ProcessorManager(), processorManager: new \App\Framework\Logging\ProcessorManager(),
contextManager: new \App\Framework\Logging\LogContextManager() contextManager: new \App\Framework\Logging\LogContextManager()

View File

@@ -15,12 +15,14 @@ use App\Framework\Random\RandomGenerator;
use App\Framework\Random\SecureRandomGenerator; use App\Framework\Random\SecureRandomGenerator;
use App\Framework\Redis\RedisConfig; use App\Framework\Redis\RedisConfig;
use App\Framework\Redis\RedisConnection; use App\Framework\Redis\RedisConnection;
use App\Framework\Redis\RedisConnectionPool;
use App\Framework\Security\CsrfTokenGenerator; use App\Framework\Security\CsrfTokenGenerator;
final readonly class SessionInitializer final readonly class SessionInitializer
{ {
public function __construct( public function __construct(
private Container $container private Container $container,
private RedisConnectionPool $redisConnectionPool,
) { ) {
} }
@@ -47,8 +49,7 @@ final readonly class SessionInitializer
throw new \RuntimeException('Redis extension not loaded'); throw new \RuntimeException('Redis extension not loaded');
} }
$redisConfig = new RedisConfig(host: 'redis', database: 3); $redisConnection = $this->redisConnectionPool->getSessionConnection();
$redisConnection = new RedisConnection($redisConfig, 'session');
$storage = new RedisSessionStorage($redisConnection); $storage = new RedisSessionStorage($redisConnection);
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Fallback to file-based storage if Redis is not available // Fallback to file-based storage if Redis is not available