Skip to content

Rotate the production secrets - #329

Merged
oskarbarcz merged 1 commit into
mainfrom
rotate-secrets
Nov 10, 2026
Merged

oskarbarcz merged 1 commit into
mainfrom
rotate-secrets

Conversation

@oskarbarcz

Copy link
Copy Markdown
Member

Why

The API started on AWS with interim secrets. The real values, freshly issued, are now on the production environment, and version 3 writes them.

What changes

  • app_secrets_version 2 → 3. The deploy rewrites every SecureString from the production secrets, and the service rolls a new task.
  • Version: 3.80.3.

The new values are the Discord bot token and client secret, the Mailgun and Skylink keys, and a refreshed tunnel token, all reset on 10 Nov. The JWT pair, ADSB_API_TOKEN and the webhook URL keep their values.

After merging

  • The Discord bot comes online, and Discord sign-in, email and Skylink lookups work again.
  • Deployments run 0 % / 100 %, so the API is down for the minute or two between tasks.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

OpenTofu plan for infra

Commit 42c7978 · Plan: 1 to add, 10 to change, 1 to destroy.

Show plan
 OpenTofu used the selected providers to generate the following execution
 plan. Resource actions are indicated with the following symbols:
!  ~ update in-place (current -> planned)
!-/+ destroy and then create replacement
 
 OpenTofu will perform the following actions:
 
   # aws_ecs_service.app will be updated in-place
!  ~ resource "aws_ecs_service" "app" {
         id                                 = "arn:aws:ecs:eu-central-1:805830022748:service/mypreflight/flight-tracker-api"
         name                               = "flight-tracker-api"
         tags                               = {}
!      ~ task_definition                    = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api:5" -> (known after apply)
!      ~ triggers                           = {
!          ~ "app_secrets_version" = "2" -> "3"
             # (1 unchanged element hidden)
         }
         # (18 unchanged attributes hidden)
 
         # (4 unchanged blocks hidden)
     }
 
   # aws_ecs_task_definition.app must be replaced
!-/+ resource "aws_ecs_task_definition" "app" {
!      ~ arn                      = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api:5" -> (known after apply)
!      ~ arn_without_revision     = "arn:aws:ecs:eu-central-1:805830022748:task-definition/flight-tracker-api" -> (known after apply)
!      ~ container_definitions    = jsonencode(
!          ~ [
                 {
                     command                = [
                         "tunnel",
                         "--no-autoupdate",
                         "--metrics",
                         "localhost:20241",
                         "--grace-period",
                         "20s",
                         "run",
                     ]
                     dependsOn              = [
                         {
                             condition     = "HEALTHY"
                             containerName = "flight-tracker-api"
                         },
                     ]
                     environment            = []
                     essential              = true
                     healthCheck            = {
                         command     = [
                             "CMD",
                             "cloudflared",
                             "tunnel",
                             "--metrics",
                             "localhost:20241",
                             "ready",
                         ]
                         interval    = 5
                         retries     = 6
                         startPeriod = 10
                         timeout     = 2
                     }
                     image                  = "cloudflare/cloudflared@sha256:4955524aeaa379e154023d5a027c250f7b35041a2e7e950cf13f144bc9b9c5ba"
                     linuxParameters        = {
                         capabilities = {
                             add  = []
                             drop = [
                                 "ALL",
                             ]
                         }
                     }
                     logConfiguration       = {
                         logDriver = "awslogs"
                         options   = {
                             awslogs-group         = "/ecs/flight-tracker-api"
                             awslogs-region        = "eu-central-1"
                             awslogs-stream-prefix = "cloudflared"
                         }
                     }
                     mountPoints            = []
                     name                   = "cloudflared"
                     portMappings           = []
                     readonlyRootFilesystem = true
                     secrets                = [
                         {
                             name      = "TUNNEL_TOKEN"
                             valueFrom = "arn:aws:ssm:eu-central-1:805830022748:parameter/mypreflight/api/cloudflare-tunnel-token"
                         },
                     ]
                     systemControls         = []
                     volumesFrom            = []
                 },
!              ~ {
!                  ~ image                  = "ghcr.io/mypreflight/flight-tracker-api:3.80.2" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.3"
                     name                   = "flight-tracker-api"
                     # (11 unchanged attributes hidden)
                 },
             ] # forces replacement
         )
!      ~ enable_fault_injection   = false -> (known after apply)
!      ~ id                       = "flight-tracker-api" -> (known after apply)
!      ~ revision                 = 5 -> (known after apply)
-      - tags                     = {} -> null
         # (11 unchanged attributes hidden)
 
-      - volume {
-          - configure_at_launch = false -> null
-          - name                = "tmp" -> null
         }
+      + volume {
+          + configure_at_launch = (known after apply)
+          + name                = "tmp"
         }
 
         # (1 unchanged block hidden)
     }
 
   # aws_ssm_parameter.cloudflare_tunnel_token will be updated in-place
!  ~ resource "aws_ssm_parameter" "cloudflare_tunnel_token" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/cloudflare-tunnel-token"
         name             = "/mypreflight/api/cloudflare-tunnel-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["ADSB_API_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/adsb-api-token"
         name             = "/mypreflight/api/adsb-api-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_APP_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-app-token"
         name             = "/mypreflight/api/discord-app-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_CLIENT_SECRET"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-client-secret"
         name             = "/mypreflight/api/discord-client-secret"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["DISCORD_WEBHOOK_URL"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/discord-webhook-url"
         name             = "/mypreflight/api/discord-webhook-url"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["JWT_PRIVATE_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/jwt-private-key"
         name             = "/mypreflight/api/jwt-private-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["JWT_PUBLIC_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/jwt-public-key"
         name             = "/mypreflight/api/jwt-public-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["MAILGUN_API_KEY"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/mailgun-api-key"
         name             = "/mypreflight/api/mailgun-api-key"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
   # aws_ssm_parameter.secret["SKYLINK_API_TOKEN"] will be updated in-place
!  ~ resource "aws_ssm_parameter" "secret" {
!      ~ has_value_wo     = true -> (known after apply)
         id               = "/mypreflight/api/skylink-api-token"
         name             = "/mypreflight/api/skylink-api-token"
         tags             = {}
!      ~ value_wo         = (write-only attribute)
!      ~ value_wo_version = 2 -> 3
         # (8 unchanged attributes hidden)
     }
 
 Plan: 1 to add, 10 to change, 1 to destroy.
 
 Changes to Outputs:
!  ~ app_image        = "ghcr.io/mypreflight/flight-tracker-api:3.80.2" -> "ghcr.io/mypreflight/flight-tracker-api:3.80.3"

Workflow run

@oskarbarcz oskarbarcz self-assigned this Nov 10, 2026
@oskarbarcz
oskarbarcz merged commit 668ab98 into main Nov 10, 2026
3 checks passed
@oskarbarcz
oskarbarcz deleted the rotate-secrets branch November 10, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant