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
6 changes: 3 additions & 3 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ Static secrets, such as API tokens and passwords, are stored as **sensitive vari

> **Note**: Some non-secret values (like `public_facing_ip`) are marked as sensitive to prevent them from being exposed in public logs or plan outputs.

### Dynamic Secrets
'''### Dynamic Secrets
For services deployed by the `proxy_service_stack` module, credentials are not stored statically. Instead:
1. **Generated Secrets**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will generate a unique, high-entropy value for each and inject it into the container's environment variables.
1. **Infisical Integration**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will fetch these secrets from Infisical Cloud and inject them into the container's environment variables.
2. **OAuth Credentials**: For services using OAuth, the `docker-stack` module automatically creates an OAuth2 provider in Authentik. The resulting `client_id` and `client_secret` are then injected as environment variables into the container.

This ensures that secrets are managed dynamically and securely, with minimal manual intervention.
This ensures that secrets are managed dynamically and securely, with minimal manual intervention.'''

## ⚠️ Operational Notes

Expand Down
2 changes: 0 additions & 2 deletions config/services/authentik.yaml

This file was deleted.

14 changes: 8 additions & 6 deletions config/services/deluge-vpn.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
deluge-vpn:
dns:
enabled: true
enabled: false
network:
internal: true
service_port: 8112
networks:
- name: "br1"
ip_address: "192.168.5.28"
auth:
enabled: true
proxy: true
enabled: false
group: "Arr"
secrets:
VPN_USER: "VPN_USER"
VPN_PASS: "VPN_PASS"
icon: "https://vectorified.com/images/deluge-icon-3.jpg"
service_name: "deluge-vpn"
image_name: "binhex/arch-delugevpn:2.2"
domain_name: "prowlarr.dcapi.app"
mounts:
- "/etc/localtime:/etc/localtime:ro"
- "/mnt/user/Arr/deluge-data:/config"
- "/mnt/user/Downloads:/data/downloads"
- "/mnt/user/appdata/binhex-delugevpn/openvpn:/config/openvpn"
- "/mnt/user/Arr/deluge-data/openvpn:/config/openvpn"
capabilities:
add:
- "CAP_NET_ADMIN"
Expand All @@ -34,4 +35,5 @@ deluge-vpn:
- "VPN_ENABLED=yes"
- "VPN_PROV=custom"
- "VPN_CLIENT=openvpn"
- "LAN_NETWORK=192.168.1.0/24"
- "LAN_NETWORK=192.168.1.0/24"
- "UPDATE_VAL=1"
12 changes: 7 additions & 5 deletions config/services/flaresolverr.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
flaresolverr:
service_name: "flaresolverr"
image_name: "flaresolverr/flaresolverr:v3.3.25"
dns:
enabled: false
network:
internal: true
service_port: 8112
networks:
- name: "br1"
ip_address: "192.168.5.27"
env:
- "TZ=America/Chicago"
ip_address: "192.168.5.29"
auth:
enabled: false
service_name: "flaresolverr"
image_name: "flaresolverr/flaresolverr:v3.3.25"
8 changes: 6 additions & 2 deletions config/services/requestrr.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
requestrr:
service_name: "requestrr"
image_name: "linuxserver/requestrr:2.1.2"
dns:
enabled: false
network:
internal: true
service_port: 4545
networks:
- name: "br1"
ip_address: "192.168.5.27"
auth:
enabled: false
service_name: "requestrr"
image_name: "linuxserver/requestrr:2.1.2"
mounts:
- "/mnt/user/Arr/requestrr-data:/config"
- "/etc/localtime:/etc/localtime:ro"
28 changes: 23 additions & 5 deletions config/stacks/arr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
ip_address: "192.168.5.22"
auth:
enabled: true
proxy: true
proxy:
enabled: true
user_secret: "prowlarr_username"
pass_secret: "prowlarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/prowlarr-icon-512x512-v9ekdjxx.png"
service_name: "prowlarr"
Expand All @@ -41,14 +44,19 @@
ip_address: "192.168.5.23"
auth:
enabled: true
proxy: true
proxy:
enabled: true
user_secret: "sonarr_username"
pass_secret: "sonarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/sonarr-icon-1024x1024-wkay604k.png"
service_name: "sonarr"
image_name: "linuxserver/sonarr:4.0.14.2939-ls281"
domain_name: "sonarr.dcapi.app"
mounts:
- "/mnt/user/Arr/sonarr-data:/config"
- "/mnt/user/Media:/media"
- "/mnt/user/Downloads:/downloads"
radarr:
dns:
enabled: true
Expand All @@ -61,14 +69,19 @@
ip_address: "192.168.5.24"
auth:
enabled: true
proxy: true
proxy:
enabled: true
user_secret: "radarr_username"
pass_secret: "radarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/radarr-icon-462x512-bydv4e4f.png"
service_name: "radarr"
image_name: "linuxserver/radarr:5.26.2"
domain_name: "radarr.dcapi.app"
mounts:
- "/mnt/user/Arr/radarr-data:/config"
- "/mnt/user/Media:/media"
- "/mnt/user/Downloads:/downloads"
lidarr:
dns:
enabled: true
Expand All @@ -81,9 +94,14 @@
ip_address: "192.168.5.25"
auth:
enabled: true
proxy: true
proxy:
enabled: true
user_secret: "lidarr_username"
pass_secret: "lidarr_password"
group: "Arr"
service_name: "lidarr"
image_name: "linuxserver/lidarr:2.13.0-develop"
mounts:
- "/mnt/user/Arr/lidarr-data:/config"
- "/mnt/user/Arr/lidarr-data:/config"
- "/mnt/user/Media:/media"
- "/mnt/user/Downloads:/downloads"
6 changes: 5 additions & 1 deletion config/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ zones:
admin_username: "DCCoder"
existing_networks:
- "br1"
- "br0"
- "br0"
infisical:
project: "home-net-ln-sy"
environment: "dev"
folder: "/secrets"
20 changes: 18 additions & 2 deletions docs/service-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ The structure of each top-level key in these files is **not standardized**. It i

Below are examples of different structures found in this project.

### Example 1: A Standalone Docker Service (`deluge-vpn`)
'''### Example 1: A Standalone Docker Service (`flaresolverr`)

This service is consumed by the `flaresolverr_service` module in `services.tf`, which is a wrapper around the generic `docker` module.

```yaml
flaresolverr:
service_name: "flaresolverr"
image_name: "flaresolverr/flaresolverr:latest"
network:
networks:
- "br1"
env:
- "LOG_LEVEL=info"
- "TZ=America/New_York"
```

### Example 2: A Standalone Docker Service (`deluge-vpn`)

This service is consumed by the `delugevpn_service` module in `services.tf`, which is a wrapper around the generic `docker` module.

Expand All @@ -31,7 +47,7 @@ deluge-vpn:
capabilities:
add:
- "NET_ADMIN"
```
```''

### Example 2: A Global Configuration Object (`authentik`)

Expand Down
8 changes: 0 additions & 8 deletions docs/stack-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,3 @@ your_stack_name:
* **`auth.oauth.redirect_uris`**: A list of additional relative paths (e.g., `/oauth/callback`) that will be appended to the service's domain name to form the complete, valid OAuth redirect URIs required by Authentik.


## Generated Secrets Workflow

* **`generated_secrets` (Stack Level)**: A list of string names (e.g., `"API_KEY"`) for secrets that your services will consume.
* **Workflow**:
1. **Definition**: You must first define these secret names in the `config/secrets.yaml` file.
2. **Generation & Storage**: The `generated_secrets` Terraform module (run as part of the root module) reads `config/secrets.yaml`, generates a unique, value for each listed secret, and then securely stores these generated values in Infisical Cloud.
3. **Consumption by Stacks**: When you list a secret name here, the `docker-stack` module will automatically pull the corresponding secret value from Infisical and inject it as an environment variable into the container.
* **Important Note**: Any secret name listed in a stack's `generated_secrets` field *must* first be defined in `config/secrets.yaml` to ensure it is generated and stored in Infisical. If a requested secret is not found, the Terraform plan will fail, preventing deployment with missing credentials.
50 changes: 50 additions & 0 deletions terraform/modules/docker-service/auth.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@


module "proxy_authentication" {
source = "../proxy_auth"
count = var.service.auth.enabled && var.service.auth.proxy.enabled ? 1 : 0

group = var.service.auth.group
description = var.service.description
# If a static IP is defined, use it. Otherwise, fall back to the service name,
# which is resolvable within a Docker network.
internal_host = "http://${coalesce(local.service_ip_address, var.service.service_name)}:${var.service.network.service_port}"
external_host = var.service.dns.domain_name
name = var.service.service_name
username_attribute = "${var.service.service_name}_username"
password_attribute = "${var.service.service_name}_password"
create_access_group = true
access_group_name = "tf_${var.service.service_name}"
user_to_add_to_access_group = var.system.network_admin_username
access_group_attributes = jsonencode(
{
"${var.service.service_name}_username" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.user_secret, null), "${var.service.service_name}_username")].value,
"${var.service.service_name}_password" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.pass_secret, null), "${var.service.service_name}_password")].value
}
)
}

module "oauth_authentication" {
source = "../oauth_auth"
count = var.service.auth.enabled && var.service.auth.oauth.enabled ? 1 : 0

group = var.service.auth.group
description = var.service.description
name = var.service.service_name
create_access_group = true
access_group_name = "tf_${var.service.service_name}"
user_to_add_to_access_group = var.system.network_admin_username
allowed_redirect_uris = concat(
[
{
matching_mode = "strict",
url = "https://${var.service.dns.domain_name}" }
],
[
for uri_path in coalesce(var.service.auth.oauth.redirect_uris, []) : {
matching_mode = "strict",
url = "https://${var.service.dns.domain_name}/${uri_path}"
}
]
)
}
21 changes: 21 additions & 0 deletions terraform/modules/docker-service/dns.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
data "nginxproxymanager_access_lists" "access_lists" {}

module "service_dns" {
source = "../dns"
count = var.service.dns.enabled ? 1 : 0

internal_only = var.service.network.internal
service_port = var.service.auth.proxy.enabled ? var.system.authentik.port : var.service.network.service_port
zone_name = var.zone_name
domain_name = var.service.dns.domain_name

# Really don't like having the ACLs hardcoded here...
access_list_id = var.service.network.internal ? local.npm_access_lists_by_name["Internal Only"] : local.npm_access_lists_by_name["Cloudflare"]
internal_host_ipv4 = var.system.proxy_ip
# If not using proxy auth, point to the service's static IP. If no static IP,
# fall back to the service name, which NPM can use as a hostname.
service_ipv4 = var.service.auth.proxy.enabled ? var.system.authentik.ip_address : coalesce(local.service_ip_address, var.service.service_name)
admin_email = var.system.network_admin_email
dns_cloudflare_api_token = var.system.cloudflare_api_token
external_host_ipv4 = var.system.public_facing_ip
}
25 changes: 25 additions & 0 deletions terraform/modules/docker-service/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
locals {
# Determine the primary IP address for the service from its network configuration.
service_ip_address = try(
try([for n in var.service.network.networks : n.ip_address if n.name == "br1" && n.ip_address != null][0], null),
try([for n in var.service.network.networks : n.ip_address if n.name == "br0" && n.ip_address != null][0], null),
try([for n in var.service.network.networks : n.ip_address if n.ip_address != null][0], null)
)

# Generate environment variables for OAuth if it's enabled for the service.
oauth_envs = var.service.auth.oauth.enabled ? [
for env_name, output_key in var.service.auth.oauth.keys :
format(
"%s=%s",
env_name,
{
"client_id" = module.oauth_authentication[0].client_id,
"client_secret" = module.oauth_authentication[0].client_secret,
"well_known_url" = module.oauth_authentication[0].provider_info_url
}[output_key]
)
] : []

# Create a map of Nginx Proxy Manager access lists by name for easy lookup.
npm_access_lists_by_name = { for al in data.nginxproxymanager_access_lists.access_lists.access_lists : al.name => al.id }
}
21 changes: 21 additions & 0 deletions terraform/modules/docker-service/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@


module "service_container" {
source = "../../modules/docker"

icon = var.service.icon
web_ui = try(var.service.network.service_port, null) != null && local.service_ip_address != null ? "http://${local.service_ip_address}:${var.service.network.service_port}" : null
container_name = var.service.service_name
container_image = var.service.image_name
container_network_mode = var.service.network_mode
enable_gpu = var.service.enable_gpu
environment_vars = toset(concat(local.secret_envs, coalesce(local.oauth_envs, []), coalesce(var.service.env, [])))
mounts = var.service.mounts
container_capabilities = var.service.capabilities
commands = var.service.commands

# Attach the container to custom networks defined in the stack, but only if the service
# explicitly lists that network in its own configuration.
# The docker module expects a list of objects with `name` and `ipv4_address`.
networks = coalesce(var.service.network.networks, [])
}
23 changes: 23 additions & 0 deletions terraform/modules/docker-service/secrets.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
data "infisical_projects" "home-net" {
# This assumes a constant project slug. Consider making this configurable
# via var.system if it needs to be more dynamic.
slug = var.system.infisical.project
}

data "infisical_secrets" "secrets" {
# Only fetch secrets if the stack configuration requests them.
count = (var.service.secrets != null && length(var.service.secrets) > 0) || (try(var.service.auth.proxy.enabled, false)) ? 1 : 0

env_slug = var.system.infisical.environment
workspace_id = data.infisical_projects.home-net.id
# This path corresponds to where the root `secrets` module stores secrets.
folder_path = var.system.infisical.folder
}

locals{
# Create a list of environment variables from the secrets map.
secret_envs = (var.service.secrets != null && length(var.service.secrets) > 0) ? [
for key, value in var.service.secrets :
format("%s=%s", key, data.infisical_secrets.secrets[0].secrets[value].value)
] : []
}
Loading