{"id":"IL-0460","org":"GoCardless","date":"2020-10-25","docs":[],"title":{"en":"Vault served an expired TLS certificate and almost every GoCardless service pod crash-looped, a 22-minute full outage","ko":"Vault 가 만료된 TLS 인증서를 계속 써서 Vault 에 기대는 서비스 파드가 연쇄로 죽고 GoCardless 서비스 전체가 22분간 멈췄다"},"domain":["kubernetes","gcp"],"impact":{"scope":"Full outage of the GoCardless API, dashboard, payment pages and operations through partner integrations for 22 minutes","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A system that almost every service depends on, such as the secrets store, can take everything down at once, and failure scenarios like Vault becoming unreachable had not been tested enough. Assuming the certificate had expired, when it had been renewed but not reloaded, added a slow and unnecessary recreation step to the recovery.","ko":"Vault 를 쓸 수 없게 된 것보다 Vault 에 기대는 모든 애플리케이션이 거의 곧바로 실패한 것이 훨씬 나빴다. 이런 장애 상황을 충분히 시험하지 않았고, 단일 장애점과 피해 범위를 줄여야 한다는 것을 장애를 겪고서야 다시 확인했다. 대응 중에는 인증서 자체가 만료됐다고 여겨 인증서를 다시 만들게 했다. 실제로는 인증서가 이미 갱신돼 있었고 Vault 가 다시 읽지 않았을 뿐이었다. 그래서 DNS 검증 때문에 몇 분 걸리는 이 단계는 건너뛸 수 있었다."},"related":[],"sources":[{"url":"https://gocardless.com/blog/incident-review-service-outage-on-25-october-2020/","kind":"blog","archived":"https://web.archive.org/web/20261006222533/https://gocardless.com/blog/incident-review-service-outage-on-25-october-2020"}],"summary":{"en":"GoCardless runs HashiCorp Vault as a highly available cluster inside Kubernetes, with Google Cloud Storage as its backend, and almost all production services read their secrets from it, authenticating with their Kubernetes service account tokens. cert-manager issues Vault's short-lived Let's Encrypt certificate. When cert-manager renewed the certificate, the new data was updated in the running container, but Vault only loads a new certificate after a restart or a SIGHUP, and no automation did that. Vault kept serving the old certificate until it expired at 07:22 on 25 October 2020.\n\nApplications get their secrets through theatre-envconsul, an in-house wrapper that runs the envconsul tool with the -once flag so that it does not restart the application whenever Vault data changes. The team had not realised that envconsul still renews its Vault leases periodically in this mode, and exits with an error, killing the application process, when a renewal fails. Vault retries had also been disabled in envconsul to work around other problems. As leases came up for renewal, the healthy pods died one by one and went into crash loops, and by 07:31 no backend could serve requests.\n\nThe on-call engineer was paged at 07:32. Believing the certificate itself had expired, the responders forced cert-manager to recreate it at 07:42, a step that was not needed and took minutes because of DNS validation. At 07:48 the Vault cluster was restarted by hand to load the new certificate, Kubernetes brought up new containers, and some pods of critical workloads were deleted to skip the 5-minute exponential backoff they had reached. Backends were serving again at 07:52.","ko":"GoCardless 는 거의 모든 운영 서비스가 기대는 비밀 관리 엔진으로 HashiCorp Vault 를 쿠버네티스 클러스터 안에서 돌렸다. Vault 의 API 와 UI 를 지키는 TLS 인증서는 cert-manager 가 Let's Encrypt 에서 받아 관리했다. cert-manager 가 인증서를 갱신하면 실행 중인 컨테이너에 마운트된 인증서 데이터는 자동으로 바뀌지만, Vault 는 이 변경을 스스로 알아채지 못한다. Vault 를 재시작하거나 SIGHUP 신호를 보내 다시 읽게 해야 새 인증서를 쓴다. GoCardless 에는 이 자동화가 없었고, Vault 는 갱신 뒤에도 옛 인증서를 계속 쓰다가 2020-10-25 07:22 에 그 인증서가 만료됐다.\n\n애플리케이션은 theatre-envconsul 이라는 자체 프로그램으로 Vault 에 인증한 뒤 공식 envconsul 을 실행해 비밀을 넣었다. GoCardless 는 envconsul 에 -once 플래그를 주어 메인 루프를 한 번만 돌게 했다. 그래도 envconsul 은 이 모드에서 Vault 리스를 주기적으로 갱신했고, 갱신이 실패하면 오류를 내고 종료하면서 관리하던 프로세스도 함께 끝냈다. GoCardless 는 다른 문제 때문에 envconsul 의 Vault 재시도를 꺼 두었다. 그래서 운영 트래픽을 받던 정상 파드가 리스가 만료되는 차례대로 하나씩 죽었고, 07:31 에 GoCardless API 요청이 전부 실패했다.\n\n07:42 에 대응자는 인증서가 만료됐다고 보고 cert-manager 로 인증서를 강제로 다시 만들었고, 07:48 에 새 인증서가 나오자 Vault 클러스터를 손으로 재시작했다. Vault 가 다시 뜬 뒤에는 쿠버네티스가 새 컨테이너를 띄워 거의 손대지 않고 복구됐다. 중요한 워크로드의 파드 몇 개는 5분까지 늘어난 지수 백오프를 건너뛰려고 직접 삭제했다. 07:52 에 백엔드 서비스가 정상으로 돌아왔고 장애는 07:54 에 끝났다."},"trigger":{"en":"At 07:22 the TLS certificate Vault was still serving expired. cert-manager had already renewed it, but Vault does not load a new certificate without a restart or a SIGHUP.","ko":"cert-manager 가 Vault 의 TLS 인증서를 갱신했지만 Vault 는 새 인증서를 다시 읽지 않고 옛 인증서를 계속 썼다. 07:22 에 그 옛 인증서가 만료됐다."},"severity":"unknown","symptoms":["service-unavailable","crash-loop","tls-certificate-errors","credentials-unavailable"],"timeline":{"detected_after":"10m","resolved_after":"32m"},"date_kind":"incident","detection":{"en":"Automated monitoring detected the failing requests and paged the on-call engineer at 07:32. Certificate expiry monitoring existed for the Layer-7 load balancers but did not cover the TCP load balancer that Vault uses.","ko":"07:32 에 자동 모니터링이 장애를 잡아 온콜 엔지니어를 호출했다."},"components":["cert-manager","secrets-store"],"root_cause":"certificate-expiry","remediation":[{"text":{"en":"Forced cert-manager to recreate the Vault certificate","ko":"07:42 에 인증서가 만료됐다고 보고 cert-manager 가 인증서를 다시 만들도록 손으로 강제했다. 실제로 인증서는 이미 갱신돼 있어 이 단계는 필요 없었다."}},{"text":{"en":"Restarted the Vault cluster by hand so it loaded the new certificate","ko":"07:48 에 새 인증서가 만들어지자 Vault 가 그 인증서를 읽도록 Vault 클러스터를 손으로 재시작했다."}},{"text":{"en":"Deleted some pods of critical workloads to skip their 5-minute exponential backoff","ko":"중요한 워크로드의 파드 몇 개를 직접 삭제해 5분까지 늘어난 지수 백오프를 건너뛰었고, 서비스가 몇 분 일찍 완전히 돌아왔다."}}],"action_items":[{"text":{"en":"Check the certificate of the TCP load balancer in front of Vault with the Blackbox exporter, as for the Layer-7 load balancers","ko":"L7 로드 밸런서에만 있던 TLS 인증서 만료 모니터링을 Vault 가 쓰는 단순 TCP 로드 밸런서까지 넓히고, Blackbox exporter 로 그 인증서도 주기적으로 검사한다."}},{"text":{"en":"Deploy the Reloader controller to roll Vault when its certificate data changes","ko":"Vault 서버가 바뀐 인증서를 스스로 다시 읽는 기능이 없으므로, Reloader 컨트롤러를 배포해 인증서 데이터가 바뀌면 Vault 를 롤링 업데이트한다."}},{"text":{"en":"Change theatre-envconsul so envconsul no longer manages the child process and only injects secrets at container startup","ko":"Vault 로 가는 네트워크 분할 같은 다른 장애가 실행 중인 서비스에 번지지 않도록, theatre-envconsul 을 바꿔 envconsul 이 자식 프로세스를 관리하지 않게 하고 컨테이너 시작 때 비밀 환경 변수를 넣는 일만 하게 한다."}}],"contributing":["single-point-of-failure","monitoring-blind-spot","assumption-unverified","manual-action-worsened"],"recurrence_of":null,"symptoms_text":{"en":"On Sunday 25 October 2020 the GoCardless API, dashboard, payment pages and partner integrations were all unavailable for 22 minutes, from 07:32 to 07:54 GMT. Automated monitoring paged the on-call engineer at 07:32, a minute after every request to the API had started failing because no healthy backend was left.\n\nThe TLS certificate that the Vault secrets server presented on its API expired at 07:22. From 07:25 application containers began crash-looping: envconsul, the tool that injects secrets from Vault, exited when it could not renew its Vault leases, took the healthy application process with it, and could not connect to Vault again on restart. Pods serving production traffic died one by one as their leases came up for renewal.\n\nThe certificate had in fact been renewed by cert-manager and the new data was mounted into the Vault container, but Vault kept serving the old certificate. The responders believed the certificate itself had expired and at 07:42 forced cert-manager to recreate it, which took several minutes because issuance uses DNS validation.","ko":"07:32 에 자동 모니터링이 장애를 잡아 온콜 엔지니어를 호출했다. 07:31 부터 GoCardless API 로 가는 요청이 전부 실패했고, 대시보드, 결제 페이지, 파트너 연동을 거치는 작업도 쓸 수 없었다. 07:32 부터 07:54 GMT 까지 22분 동안 서비스 전체가 멈췄다.\n\n07:22 에 Vault 의 인증서가 만료됐고, 07:25 부터 컨테이너가 크래시 루프에 들어갔다. envconsul 이 Vault 리스 갱신에 실패하자 오류를 내고 종료하면서 자신이 관리하던 정상 프로세스까지 함께 끝냈다. 다시 시작한 컨테이너는 Vault 에 새로 연결하지 못했다.\n\n운영 트래픽을 받던 정상 파드들이 Vault 리스가 만료되는 차례대로 하나씩 죽었다. 결국 요청을 받을 정상 백엔드가 하나도 남지 않았다.\n\n대응자는 TLS 인증서 자체가 만료됐다고 보고 cert-manager 로 인증서를 강제로 다시 만들었다. 실제로 인증서는 이미 갱신돼 있었고 Vault 가 새 인증서를 다시 읽지 않았을 뿐이었다. 인증서를 다시 만들려면 DNS 검증을 거쳐야 했고, 레코드가 전파되는 데 몇 분이 걸렸다."},"updated_at":"2026-11-14T12:34:10.853427Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0460","org_slug":"gocardless"}