{"id":"IL-0506","org":"UC Berkeley","date":"2017-02-09","docs":["https://github.com/jupyterhub/jupyterhub/issues/986","https://github.com/jupyterhub/jupyterhub/issues/985","https://github.com/jupyterhub/kubespawner/issues/21","https://github.com/jupyterhub/oauthenticator/issues/65"],"title":{"en":"UC Berkeley DataHub logins failed and students got empty home directories after the JupyterHub database was overwritten","ko":"UC Berkeley DataHub 의 JupyterHub sqlite 데이터베이스가 빈 데이터베이스로 덮어써져 로그인이 실패하고 홈 디렉터리가 비어 보이고 다른 사용자로 로그인되는 문제가 났다"},"domain":["kubernetes","database"],"impact":{"scope":"UC Berkeley DataHub: logins failed with proxy errors, users saw empty home directories, and some were logged in as another user","users":"DataHub users, about 1000 accounts","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":null,"related":[],"sources":[{"url":"https://github.com/berkeley-dsep-infra/datahub/blob/staging/docs/incidents/2017-02-09-datahub-db-outage.qmd","kind":"github","archived":null}],"summary":{"en":"Shortly after midnight on 9 February 2017, an operator debugging earlier 400 errors opened a shell in the DataHub hub pod, installed sqlite3, attached jupyterhub.sqlite and changed a user's server entry to see whether a wrong value would cause a problem. When nothing changed for the account, the operator started sqlite3 again without attaching the database and ran `.save jupyterhub.sqlite`, which overwrote the database file with an empty one; sqlite3 does this without any confirmation. All tables were gone. Users who logged in got new user ids, and because PVC names were built from the user id, the hub gave them new, empty disks.\n\nAt 1:28 AM the hub was scaled to 0 replicas and the team rebuilt the sqlite database from the Kubernetes PVCs, whose names contain a sanitized form of the username and the user id. The rebuilt database was attached at 2:42 AM and the admin page showed about 1000 users again. In the rebuild, cookie_id was left empty for many users, although JupyterHub expects it always to be a uuid, and everyone with an empty cookie_id was logged in as the first such user in the table. Changing one character near the end of the cookie secret had no effect, because JupyterHub uses only part of the secret; changing one at the beginning did log everyone out. In the meantime students were advised to use ds8.berkeley.edu, which crashed because it lacked resources for a data8-level class.\n\nAt 10:45 AM a script filled in cookie_id for all users, the hub was restarted and all user pods were deleted, and most users were back. Users with a '.' in their name had been rebuilt with a '-' instead, so those users attached to new empty PVCs; at 11:15 AM a script dropped the new users with '.' and renamed the older '-' users to use '.'. The all-clear was given at 11:17 AM.","ko":"2017년 2월 9일 0:25 무렵, 엔지니어가 앞서 난 400 오류를 디버깅하려고 hub 파드에서 sqlite3 로 servers 테이블의 ip 와 base_url 을 일부러 틀리게 바꿔 봤다. 바뀐 것이 없어 보이자 새 sqlite3 세션에서 .save jupyterhub.sqlite 를 실행했고, 먼저 ATTACH 를 하지 않았기 때문에 JupyterHub sqlite 데이터베이스가 빈 데이터베이스로 덮어써졌다. 나중에 로컬에서 sqlite3 의 .save <filename> 이 확인 없이 파일을 덮어쓴다는 것을 확인했다.\n\n테이블이 모두 사라지자 hub 는 sqlite 오류를 냈고 사용자는 프록시 오류를 받았다. 로그인한 사용자는 새 사용자 id 를 받았고, 그 때문에 새 빈 디스크가 만들어졌다. 1:28 에 hub 파드의 replicas 를 0으로 줄이고, PVC 이름에 담긴 사용자 이름과 사용자 id 를 짝지어 데이터베이스를 다시 만들었다. 2:42 에 다시 만든 데이터베이스를 붙이자 관리자 화면의 사용자 수가 1000명쯤으로 돌아왔다.\n\n다시 만든 데이터베이스에서는 여러 사용자의 cookie_id 가 비어 있었다. 이 사용자들은 id 순으로 맨 앞에 있고 cookie_id 가 빈 사용자 한 명의 노트북으로 로그인됐다. 10:11 에 cookie secret 을 바꿔 모두 로그아웃시키려 했는데, 끝 근처 글자를 바꿨을 때는 효과가 없었고 앞쪽 글자를 바꾸자 로그아웃됐다. JupyterHub 는 cookie secret 의 일정 길이만 쓰고 나머지를 버렸다. 10:45 에 모든 사용자의 cookie_id 를 채우고 hub 를 재시작한 뒤 사용자 파드를 다시 모두 지우자 대부분의 사용자가 돌아왔다.\n\n이름에 '.' 이 들어간 사용자는 옛 레코드에 '.' 대신 '-' 가 들어가 있었고, 로그인하면서 '.' 이 들어간 새 사용자가 생겨 빈 PVC 를 받았다. 11:15 에 '.' 이 들어간 새 사용자를 지우고 '-' 를 '.' 으로 바꾸는 스크립트를 돌렸고, 11:17 에 DataHub 가 모두 복구됐다고 알렸다. 그 사이 학생들에게 ds8.berkeley.edu 를 쓰라는 안내가 나갔지만, ds8.berkeley.edu 는 data8 규모 수업을 감당할 자원이 없어 곧 다운됐다."},"trigger":{"en":"While debugging, an operator ran `.save jupyterhub.sqlite` in sqlite3 inside the hub pod without having attached the database, which replaced the JupyterHub database file with an empty one.","ko":"엔지니어가 앞서 난 400 오류를 디버깅하려고 hub 파드 안에서 sqlite3 를 다시 열고, 데이터베이스를 ATTACH 하지 않은 채 .save jupyterhub.sqlite 를 실행했다. 이 명령이 JupyterHub 데이터베이스 파일을 빈 데이터베이스로 덮어썼다."},"severity":"unknown","symptoms":["service-unavailable","data-loss","requests-misrouted"],"timeline":{"detected_after":"35m","resolved_after":"10h 52m"},"date_kind":"incident","detection":{"en":"DataHub was reported down at about 1 AM, when users got a proxy error at login. At 1:12 AM a user reported that he could log in but all his notebooks were gone.","ko":"새벽 1시쯤 DataHub 가 다운됐다는 신고가 들어왔다. 1:12 에는 한 사용자가 로그인은 되지만 노트북이 모두 사라졌다고 알렸다."},"components":["database","persistent-volume","jupyterhub"],"root_cause":"human-operation","remediation":[{"text":{"en":"Scaled the hub to 0 replicas","ko":"hub 파드의 replicas 를 0으로 줄여 hub 를 내렸다."}},{"text":{"en":"Rebuilt the JupyterHub sqlite database by matching Kubernetes PVC names back to user ids","ko":"쿠버네티스 PVC 를 사용자 id 와 다시 짝지어 JupyterHub sqlite 데이터베이스를 다시 만들었다. PVC 이름에 정리된 사용자 이름과 사용자 id 가 들어 있어서 할 수 있었다."}},{"text":{"en":"Set cookie_id to null instead of a dummy value and attached the rebuilt database to the hub","ko":"처음 넣은 가짜 cookie_id 값을 null 로 바꾸고, sqlite 파일을 DataHub 에 다시 붙였다."}},{"text":{"en":"Changed the cookie secret in the hub pod and restarted it to log all users out","ko":"모든 사용자를 로그아웃시키려고 hub 파드의 /srv/jupyterhub/jupyterhub_cookie_secret 을 바꿨다. 끝 근처 한 글자를 바꿨을 때는 효과가 없었고, 앞쪽 한 글자를 바꾸고 재시작하자 모두 로그아웃됐다."}},{"text":{"en":"Deleted all user pods to log everyone out","ko":"사용자 파드를 모두 지워 모든 사용자를 확실히 로그아웃시켰다."}},{"text":{"en":"Ran a script that filled in cookie_id for all users, restarted the hub and deleted all user pods again","ko":"모든 사용자의 cookie_id 를 채우는 스크립트를 돌렸고, 메모리에 남은 오래된 캐시를 없애려고 hub 를 재시작한 뒤 사용자 파드를 다시 모두 지웠다."}},{"text":{"en":"Dropped the new users with a '.' in their name and renamed the older users from '-' to '.'","ko":"이름에 '.' 이 들어간 새 사용자를 지우고, 이름에 '-' 가 있는 모든 사용자의 '-' 를 '.' 으로 바꾸는 스크립트를 돌렸다."}}],"action_items":[{"text":{"en":"Report upstream that JupyterHub uses only part of the cookie secret and discards the rest","ko":"JupyterHub 가 cookie secret 의 일정 길이만 쓰고 나머지를 버려서, 로그아웃시키려고 값을 바꿀 때 혼란이 생긴다는 문제를 JupyterHub 에 보고한다."}},{"text":{"en":"Report upstream that the cookie_id column of the users table should have UNIQUE and NOT NULL constraints","ko":"users 테이블의 cookie_id 열에 UNIQUE 와 NOT NULL 제약을 두자고 JupyterHub 에 제안한다."}},{"text":{"en":"Ask KubeSpawner to support username hashes in PVC and pod names so the names stay the same when the database is deleted","ko":"데이터베이스가 지워져도 파드와 PVC 이름이 그대로 남도록, 이름에 사용자 id 대신 사용자 이름 해시를 쓰는 기능을 KubeSpawner 에 요청한다."}},{"text":{"en":"Ask OAuthenticator to support using the id from the Google authenticator as the user id, giving stable user ids","ko":"사용자 테이블의 id 를 Google 인증이 주는 'id' 와 같게 두어, 처음 로그인한 때와 상관없이 사용자 id 가 바뀌지 않게 하는 기능을 OAuthenticator 에 요청한다."}},{"text":{"en":"Switch to Google Cloud SQL, a hosted and managed MySQL database","ko":"관리형 MySQL 서비스인 Google Cloud SQL 로 옮긴다."}},{"text":{"en":"Take regular and tested backups of the database","ko":"데이터베이스를 정기적으로 백업하고 그 백업을 시험한다."}},{"text":{"en":"Start an operational FAQ of things to do and not to do","ko":"해야 할 일과 하지 말아야 할 일을 담은 운영 FAQ 를 쓰기 시작한다."}},{"text":{"en":"Set up better monitoring and paging","ko":"모니터링과 호출 체계를 개선한다."}},{"text":{"en":"Document escalation procedures explicitly","ko":"에스컬레이션 절차를 분명하게 문서로 남긴다."}}],"contributing":["no-safeguard-on-destructive-change","unsafe-failure-mode","manual-action-worsened"],"recurrence_of":null,"symptoms_text":{"en":"At about 1 AM on 9 February 2017, UC Berkeley's DataHub was reported down: users trying to log in got a proxy error. The hub pod was running, but its log was full of sqlite errors.\n\nFrom 0:25:59 AM the hub logs showed \"sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: proxies\", and the same error followed for the hub table, the two tables used most often.\n\nAt 1:12 AM a student could log in but found all his notebooks gone. The JupyterHub admin panel listed only about 50 users where there had been about 1000, and `kubectl get pvc` showed that the student now had two PVCs, one 21 days old and one 5 minutes old: the hub had created a new account and a new, empty persistent disk because it had lost track of the old one.\n\nAfter the database was rebuilt, some users were logged in as one particular other user. Changing the cookie secret and deleting all user pods logged everyone out but did not stop it. Later, users with a '.' in their username still got an empty home directory after everyone else was fine.","ko":"새벽 1시쯤 DataHub 에 로그인하려는 사용자가 프록시 오류를 받았다. hub 파드는 떠 있었지만 로그가 sqlite 오류로 가득했다.\n\nhub 로그의 오류는 sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: proxies [SQL: 'SELECT proxies.id AS proxies_id, proxies._public_server_id AS proxies__public_server_id, proxies._api_server_id AS proxies__api_server_id \\nFROM proxies \\nWHERE proxies.id = ?'] [parameters: (1,)] 였다. 가장 자주 쓰이는 hub 테이블에도 같은 오류가 이어졌다.\n\n1:12 에 한 사용자가 로그인은 되지만 노트북이 모두 사라졌다고 알렸다. JupyterHub 관리자 화면의 사용자는 1000명쯤에서 50명쯤으로 줄어 있었다. 사용자 데이터베이스가 지워져서 hub 가 로그인한 사용자를 새 계정으로 만들었다. hub 는 예전 디스크를 찾지 못해 새 영구 디스크를 만들었고, 그래서 홈 디렉터리가 비어 보였다.\n\nkubectl --namespace=datahub get pvc 로 보면 같은 사용자의 PVC 가 claim-<username>-257 과 claim-<username>-51 두 개였고, 뒤의 것은 만든 지 5분밖에 되지 않았다.\n\n데이터베이스를 다시 만든 뒤에도 일부 사용자가 로그인하면 특정 사용자 한 명의 계정으로 들어갔다. cookie secret 을 바꿔 모두 로그아웃시키고 사용자 파드를 모두 지워도 계속됐다.\n\n다른 사용자가 모두 정상이 된 뒤에도 이름에 '.' 이 들어간 사용자는 빈 홈 디렉터리를 받았다."},"updated_at":"2026-11-14T12:34:15.097517Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0506","org_slug":"uc-berkeley"}