{"id":25696,"date":"2026-10-07T14:26:51","date_gmt":"2026-10-07T20:26:51","guid":{"rendered":"https:\/\/sada.services\/?p=25696"},"modified":"2026-10-07T14:36:52","modified_gmt":"2026-10-07T20:36:52","slug":"semaphore-consultar-las-claves","status":"publish","type":"post","link":"https:\/\/sada.services\/?p=25696","title":{"rendered":"Rotaci\u00f3n de las claves de Semaphore \u2014 ops.una.ac.cr"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Aplicado el&nbsp;<strong>7 de octubre de 2026<\/strong>. Este documento sirve tanto de registro de lo que se hizo como de procedimiento para la pr\u00f3xima vez.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">El problema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El fichero&nbsp;<code>config.json.bak<\/code>&nbsp;estaba&nbsp;<strong>versionado en git desde el primer commit<\/strong>&nbsp;del repositorio&nbsp;<code>INFRA\/ansible<\/code>, y conten\u00eda las cuatro claves que Semaphore usaba en ese momento, todas vivas:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Clave<\/th><th class=\"has-text-align-left\" data-align=\"left\">Qu\u00e9 protege<\/th><\/tr><\/thead><tbody><tr><td><code>postgres.pass<\/code><\/td><td>la base de datos de Semaphore<\/td><\/tr><tr><td><code>cookie_encryption<\/code><\/td><td>cifra las cookies de sesi\u00f3n<\/td><\/tr><tr><td><code>cookie_hash<\/code><\/td><td>firma las cookies de sesi\u00f3n<\/td><\/tr><tr><td><strong><code>access_key_encryption<\/code><\/strong><\/td><td><strong>cifra el Key Store entero<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">La \u00faltima es la grave. Con ella, m\u00e1s un volcado de la base \u2014cuya contrase\u00f1a estaba en el mismo fichero\u2014, se descifran las&nbsp;<strong>51 credenciales<\/strong>&nbsp;del Key Store: las llaves SSH de los 96 servidores gestionados, las contrase\u00f1as de&nbsp;<code>ansible-vault<\/code>&nbsp;y las de los dos vCenter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El fichero estaba en Bitbucket y en todos los clones del repositorio.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Por qu\u00e9 NO se trunc\u00f3 el historial de git<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Fue la primera idea y se descart\u00f3, por una raz\u00f3n concreta:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Un secreto publicado no se despublica.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Quien ya hab\u00eda clonado el repositorio\u00a0<strong>tiene las cuatro claves en su\u00a0<code>.git<\/code><\/strong>. Un\u00a0<code>force push<\/code>\u00a0no le toca nada.<\/li>\n\n\n\n<li>Bitbucket\u00a0<strong>conserva un tiempo los commits sin referencia<\/strong>, y siguen siendo accesibles por su SHA. No desaparecen porque se reescriba\u00a0<code>master<\/code>.<\/li>\n\n\n\n<li>Truncar costaba\u00a0<strong>todo el historial<\/strong>\u00a0del repositorio.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Es decir: se pagaba un precio alto y&nbsp;<strong>las claves segu\u00edan sirviendo<\/strong>. La pregunta no era c\u00f3mo borrarlas del repositorio, sino c\u00f3mo hacer que dejaran de servir. Lo \u00fanico que lo consigue es&nbsp;<strong>rotarlas<\/strong>. Una vez rotadas, lo que quede en el historial ya no abre nada.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lo que se tem\u00eda, y por qu\u00e9 no pas\u00f3<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La preocupaci\u00f3n era&nbsp;<code>access_key_encryption<\/code>: si se cambia, Semaphore ya no puede descifrar el Key Store, y parec\u00eda que habr\u00eda que volver a meter las&nbsp;<strong>51 credenciales a mano<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No hizo falta. Semaphore sabe recifrarlo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>semaphore vaults rekey --old-key &lt;clave-vieja&gt;\n<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><em>Re-encrypt Key Store in database with using current encryption key<\/em><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Se cambia la clave en&nbsp;<code>config.json<\/code>, se corre eso, y recifra las 51 sin tocar ninguna.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">El procedimiento<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El script completo est\u00e1 en&nbsp;<strong><code>\/home\/cgi\/rotar_claves_semaphore.sh<\/code><\/strong>. Se corre como root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>su -c 'bash \/home\/cgi\/rotar_claves_semaphore.sh'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lo que hace, en orden:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">#<\/th><th class=\"has-text-align-left\" data-align=\"left\">Paso<\/th><th class=\"has-text-align-left\" data-align=\"left\">Detalle<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td><strong>Respaldos<\/strong><\/td><td><code>config.json<\/code>&nbsp;y un volcado de la base,&nbsp;<strong>fuera del repositorio<\/strong>, en&nbsp;<code>\/home\/cgi\/respaldos\/<\/code>. Verifica que el volcado trae la tabla&nbsp;<code>access_key<\/code>&nbsp;antes de seguir<\/td><\/tr><tr><td>2<\/td><td>Genera 4 claves nuevas<\/td><td><code>openssl rand -base64 32<\/code>&nbsp;para las tres de 44 caracteres<\/td><\/tr><tr><td>3<\/td><td><strong>Para Semaphore<\/strong><\/td><td>Si sigue corriendo mientras se recifra, \u00e9l tiene la clave vieja en memoria y la base pasa a la nueva: cualquier tarea en esa ventana falla<\/td><\/tr><tr><td>4<\/td><td>Contrase\u00f1a de PostgreSQL<\/td><td><code>ALTER USER<\/code>&nbsp;+ actualiza&nbsp;<code>\/home\/cgi\/.pgpass_semaphore<\/code><\/td><\/tr><tr><td>5<\/td><td>Reescribe&nbsp;<code>config.json<\/code><\/td><td>Las cuatro claves. Comprueba despu\u00e9s que la base acepta la nueva<\/td><\/tr><tr><td>6<\/td><td><strong>Recifra el Key Store<\/strong><\/td><td><code>semaphore vaults rekey --old-key<\/code>. Si falla,&nbsp;<strong>aborta y dice qu\u00e9 restaurar<\/strong><\/td><\/tr><tr><td>7<\/td><td>Levanta Semaphore<\/td><td>Y verifica que qued\u00f3 activo<\/td><\/tr><tr><td>8<\/td><td>Comprueba<\/td><td>Que siguen siendo 51 credenciales<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Impacto<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Semaphore parado unos minutos.<\/li>\n\n\n\n<li>\ud83d\udd34\u00a0<strong>Todos tienen que volver a entrar<\/strong>\u00a0a la interfaz: las cookies cambian.<\/li>\n\n\n\n<li>Las credenciales del Key Store siguen funcionando.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udd34 La comprobaci\u00f3n que de verdad valida el cambio<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Que el script termine bien&nbsp;<strong>no basta<\/strong>. Un&nbsp;<code>rekey<\/code>&nbsp;a medias no se ve al arrancar.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hay que correr una tarea real que use una llave SSH del Key Store.<\/strong>&nbsp;Si alguna qued\u00f3 mal recifrada, se ve ah\u00ed: la tarea no puede conectarse al servidor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El 2026-10-07 se valid\u00f3 con la plantilla&nbsp;<code>PB | PROD | ADMISI\u00d3N | DEBIAN | USOK | N: PROD-ADMISION-N01<\/code>, que usa la llave 43:&nbsp;<code>ok=32 changed=0 failed=0 unreachable=0<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udd11 C\u00f3mo consultar las claves actuales<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Contrase\u00f1a de PostgreSQL<\/em>\ncut -d: -f5 \/home\/cgi\/.pgpass_semaphore\n\n<em># Las tres de config.json<\/em>\npython3 -c \"import json; c=json.load(open('\/home\/cgi\/semaphore\/config.json')); \\\n&#91;print('%-24s %s' % (k, c&#91;k])) for k in ('cookie_encryption','cookie_hash','access_key_encryption')]\"\n\n<em># Datos de conexi\u00f3n a la base, sin la contrase\u00f1a<\/em>\npython3 -c \"import json; pg=json.load(open('\/home\/cgi\/semaphore\/config.json'))&#91;'postgres']; \\\nprint('host:', pg&#91;'host'], ' base:', pg&#91;'name'], ' usuario:', pg&#91;'user'])\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u26a0\ufe0f&nbsp;<strong><code>access_key_encryption<\/code>&nbsp;es la cr\u00edtica.<\/strong>&nbsp;Si se pierde, el Key Store queda ilegible y hay que volver a meter las 51 credenciales a mano. Gu\u00e1rdela donde no se pierda \u2014TeamPass o equivalente\u2014 y&nbsp;<strong>nunca en el repositorio<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Comprobar que una clave antigua ya no sirve<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 -c \"\nimport json, hashlib\nh = lambda s: hashlib.sha256(s.encode()).hexdigest()&#91;:16]\nc = json.load(open('\/home\/cgi\/semaphore\/config.json'))\nvieja = input('clave antigua a comprobar: ')\nprint('SIGUE VIVA' if any(h(vieja)==h(v) for v in\n      &#91;c&#91;'postgres']&#91;'pass'], c&#91;'cookie_encryption'], c&#91;'cookie_hash'], c&#91;'access_key_encryption']])\n      else 'ya no sirve')\"\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Vuelta atr\u00e1s<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Los respaldos quedan en&nbsp;<code>\/home\/cgi\/respaldos\/<\/code>&nbsp;con la fecha en el nombre y permisos&nbsp;<code>600<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl stop semaphore\ncp \/home\/cgi\/respaldos\/config.json.pre-rotacion-&lt;fecha&gt; \/home\/cgi\/semaphore\/config.json\nzcat \/home\/cgi\/respaldos\/semaphore_db_pre-rotacion-&lt;fecha&gt;.sql.gz \\\n  | PGPASSFILE=\/home\/cgi\/.pgpass_semaphore psql -h 127.0.0.1 -U semaphore_user -d semaphore_db\nsystemctl start semaphore\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Despu\u00e9s de rotar<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Los dos\u00a0<code>config.json.bak*<\/code>\u00a0salieron del repositorio. El\u00a0<code>.gitignore<\/code>\u00a0ya bloquea\u00a0<code>config.json.bak*<\/code>, para que no vuelvan a entrar.<\/li>\n\n\n\n<li>Quedan copias de consulta en\u00a0<code>\/home\/cgi\/respaldos\/config-historicos\/<\/code>, con permisos\u00a0<code>600<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Un fallo del script, por si reaparece<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El primer intento abort\u00f3 con&nbsp;<em>\u00abel volcado no trae access_key\u00bb<\/em>&nbsp;aunque el volcado&nbsp;<strong>s\u00ed<\/strong>&nbsp;lo tra\u00eda. La causa:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set -o pipefail\nzcat fichero_grande.gz | grep -q PATRON     <em># devuelve 141 CUANDO EL PATR\u00d3N S\u00cd EST\u00c1<\/em>\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>grep -q<\/code>&nbsp;sale en cuanto encuentra la primera coincidencia y cierra la tuber\u00eda.&nbsp;<code>zcat<\/code>, que iba por el 1% del fichero, recibe&nbsp;<strong>SIGPIPE<\/strong>&nbsp;y muere con 141. Con&nbsp;<code>pipefail<\/code>, ese 141 es el c\u00f3digo del pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Con ficheros peque\u00f1os no se reproduce \u2014<code>grep<\/code>&nbsp;los consume enteros antes de salir\u2014, as\u00ed que un ensayo con pocas l\u00edneas lo da por bueno. La forma correcta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>n=$(zcat fichero_grande.gz | grep -c PATRON || true)\n&#91; \"${n:-0}\" -ge 1 ] || { echo \"no est\u00e1\"; exit 2; }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">El mismo problema afecta a cualquier&nbsp;<code>| head<\/code>,&nbsp;<code>| grep -q<\/code>&nbsp;o&nbsp;<code>| first-match<\/code>&nbsp;sobre algo grande con&nbsp;<code>pipefail<\/code>&nbsp;activo.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aplicado el&nbsp;7 de octubre de 2026. Este documento sirve tanto de registro de lo que se hizo como de procedimiento para la pr\u00f3xima vez. El problema El fichero&nbsp;config.json.bak&nbsp;estaba&nbsp;versionado en git desde el primer commit&nbsp;del repositorio&nbsp;INFRA\/ansible, y conten\u00eda las cuatro claves que Semaphore usaba en ese momento, todas vivas: Clave Qu\u00e9 protege postgres.pass la base de [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-25696","post","type-post","status-publish","format-standard","hentry","category-sin-categoria"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/posts\/25696","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sada.services\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=25696"}],"version-history":[{"count":3,"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/posts\/25696\/revisions"}],"predecessor-version":[{"id":25699,"href":"https:\/\/sada.services\/index.php?rest_route=\/wp\/v2\/posts\/25696\/revisions\/25699"}],"wp:attachment":[{"href":"https:\/\/sada.services\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=25696"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sada.services\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=25696"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sada.services\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=25696"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}