Splunk certbot et Let’s Encrypt App

Splunk certbot et Let's Encrypt

Je continue un peu sur Splunk, ce coup-ci j’ai fait une TA Splunk certbot et Let’s Encrypt. J’en ai eu un peu marre de déboguer les enrôlements certbot sans Splunk. Donc comme la dernière fois, j’ai fait une TA supportant le format de log de certbot actuel (versions 0.28.0). J’ai appliqué la même démarche que pour VSFTPD et fail2ban avec le Splunk Add-on Builder.

CLI Packaging

A la nuance qu’au moment de publier, j’ai fait l’update vers Splunk 8 qui m’a cassé le Add-on Builder… du coup j’ai du terminer en mode fichier et faire mes dernières modifs en mode CLI et par exemple packager avec :

./splunk package app TA-certbot

TLDR, elle est où cette TA Splunk certbot et Let’s Encrypt ?

Comme d’habitude, vous la trouverez sur la Splunkbase :

https://splunkbase.splunk.com/app/4758/

Modification de Limits.conf

Notez qu’il y a un configuration spécifique pour cette app. La regex que j’utilise est « un peu violente » (cf. explications ci-dessous). Du coup pour quelle soit fonctionnelle, il se peut (mais à priori ça marche quand même sans) que vous deviez modifier votre fichier limits.conf et rajouter le stanza suivant :

[rex]
depth_limit = 100000

Ce réglages permet d’augmenter la récursivité maximale des expressions régulières dans Splunk et donc de permettre des formes plus complexes… aux détriments des performances.

Les logs let’s encrypt n’étant pas extrêmement volumineux, j’aurais tendance à dire que ce n’est pas trop risqué. Garder quand même à l’esprit que ce réglage est global, et pas juste pour mon App. Enfin, si vous gérez une infra de 100 000 sites certifiés par letsencrypt et supervisés avec un seul Splunk : je pense qu’il vous sera nécessaire de revoir ma regex… ou votre infra !

Regex d’extraction des champs

Alors les logs certbot ont le format suivant voici un petit exemple blanchi :

2019-10-14 11:07:08,727:DEBUG:certbot.cli:Var dry_run=True (set by user).
2019-10-14 11:07:08,727:DEBUG:certbot.cli:Var server={'dry_run', 'staging'} (set by user).
2019-10-14 11:07:08,727:DEBUG:certbot.cli:Var dry_run=True (set by user).
2019-10-14 11:07:08,728:DEBUG:certbot.cli:Var server={'dry_run', 'staging'} (set by user).
2019-10-14 11:07:08,728:DEBUG:certbot.cli:Var account={'server'} (set by user).
2019-10-14 11:07:08,757:INFO:certbot.renewal:Cert not due for renewal, but simulating renewal for dry run
2019-10-14 11:07:08,757:DEBUG:certbot.plugins.selection:Requested authenticator nginx and installer nginx
2019-10-14 11:07:10,881:DEBUG:certbot.plugins.selection:Single candidate plugin: * nginx
Description: Nginx Web Server plugin
Interfaces: IAuthenticator, IInstaller, IPlugin
Entry point: nginx = certbot_nginx.configurator:NginxConfigurator
Initialized: 
Prep: True
2019-10-14 11:07:10,888:DEBUG:certbot.plugins.selection:Single candidate plugin: * nginx
Description: Nginx Web Server plugin
Interfaces: IAuthenticator, IInstaller, IPlugin
Entry point: nginx = certbot_nginx.configurator:NginxConfigurator
Initialized: 
Prep: True
2019-10-14 11:07:10,889:DEBUG:certbot.plugins.selection:Selected authenticator  and installer 
2019-10-14 11:07:10,889:INFO:certbot.plugins.selection:Plugins selected: Authenticator nginx, Installer nginx
2019-10-14 11:07:10,902:DEBUG:certbot.main:Picked account: )>), only_return_existing=None, contact=('mailto:trol@yopmail.com',), agreement='https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf')), 019763ae9d0030f2ffa8f8cb689e7976, Meta(creation_dt=datetime.datetime(2018, 2, 27, 14, 17, 55, tzinfo=), creation_host='sd-99715.dedibox.fr'))>
2019-10-14 11:07:10,911:DEBUG:acme.client:Sending GET request to https://acme-staging-v02.api.letsencrypt.org/directory.
2019-10-14 11:07:10,998:DEBUG:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): acme-staging-v02.api.letsencrypt.org
2019-10-14 11:07:14,617:DEBUG:requests.packages.urllib3.connectionpool:https://acme-staging-v02.api.letsencrypt.org:443 "GET /directory HTTP/1.1" 200 724
2019-10-14 11:07:14,619:DEBUG:acme.client:Received response:
HTTP 200
Server: nginx
Date: Mon, 14 Oct 2019 09:07:14 GMT
Content-Type: application/json
Content-Length: 724
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
{
   "kRr68eKT_x4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
   "keyChange": "https://acme-staging-v02.api.letsencrypt.org/acme/key-change",
   "meta": {
     "caaIdentities": [
       "letsencrypt.org"
     ],
     "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
     "website": "https://letsencrypt.org/docs/staging-environment/"
   },
   "newAccount": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct",
   "newNonce": "https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce",
   "newOrder": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order",
   "revokeCert": "https://acme-staging-v02.api.letsencrypt.org/acme/revoke-cert"
}
2019-10-14 11:07:14,620:INFO:certbot.main:Renewing an existing certificate
2019-10-14 11:07:14,999:DEBUG:acme.client:Requesting fresh nonce
2019-10-14 11:07:15,006:DEBUG:acme.client:Sending HEAD request to https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce.
2019-10-14 11:07:15,156:DEBUG:requests.packages.urllib3.connectionpool:https://acme-staging-v02.api.letsencrypt.org:443 "HEAD /acme/new-nonce HTTP/1.1" 200 0
2019-10-14 11:07:15,158:DEBUG:acme.client:Received response:
HTTP 200
Server: nginx
Date: Mon, 14 Oct 2019 09:07:15 GMT
Connection: keep-alive
Cache-Control: public, max-age=0, no-cache
Link: https://acme-staging-v02.api.letsencrypt.org/directory;rel="index"
Replay-Nonce: 0002fqXrdxZwV_xxxxx-xxxxxxxx-KdWhvOCtgFh5YNNVM
X-Frame-Options: DENY
Strict-Transport-Security: max-age=604800
2019-10-14 11:07:15,158:DEBUG:acme.client:Storing nonce:  0002fqXrdxZwV_xxxxx-xxxxxxxx-KdWhvOCtgFh5YNNVM 
2019-10-14 11:07:15,160:DEBUG:acme.client:JWS payload:
b'{\n  "identifiers": [\n    {\n      "type": "dns",\n      "value": "site.tld"\n    },\n    {\n      "type": "dns",\n      "value": "cdn-css. site.tld "\n    },\n    {\n      "type": "dns",\n      "value": "cdn-js-body. site.tld "\n    },\n    {\n      "type": "dns",\n      "value": "cdn-js-footer. site.tld "\n    },\n    {\n      "type": "dns",\n      "value": "cdn-js-head. site.tld "\n    },\n    {\n      "type": "dns",\n      "value": "www. site.tld "\n    }\n  ]\n}'
2019-10-14 11:07:15,173:DEBUG:acme.client:Sending POST request to https://acme-staging-v02.api.letsencrypt.org/acme/new-order:
{
   "protected": "SOmeBASE64Str",
   "payload": "ANOTHERB64STR",
   "signature": "YETANOTHERB64STR"
}
2019-10-14 11:07:15,387:DEBUG:requests.packages.urllib3.connectionpool:https://acme-staging-v02.api.letsencrypt.org:443 "POST /acme/new-order HTTP/1.1" 201 1104

Du coup, pour le sourcetype c’est facile, le strptime format string est égal à :

%Y-%m-%d %H:%M:%S.%3

Il est pertinent de rajouter un « timestamppréfix » à « ^ » obligeant un match en début de ligne car on est pas à l’abri de trouver une date dans le contenu des logs. Dans la même veine, régler le lookahead 32 vu qu’on a pas besoin d’aller plus loin pour trouver la date.

A hell of a regex…

Par contre l’extraction des champs. Là, c’est beaucoup moins trivial, on a des logs :

  • dont les principaux éléments sont séparés par « : » ;
  • mais pour lequel on peut trouver dans la ligne des URL avec plusieurs fois des « : » qui ne correspondent pas au changement de champs.
  • Qui sont parfois sur plusieurs lignes… mais pas systématiquement.
  • Et le dernier champs sur la première ligne n’est pas toujours présent, indépendamment du fait qu’il y ai encore des lignes en dessous qui appartiennent au même log…

En luttant un peu, j’ai fini par sortir l’expression suivante qui fonctionne pas trop mal.

^(?<vendor_date>\d{4}(?:\-\d\d){2} [^,]+,[^:]+):(?<vendor_level>[^:]+):(?<vendor_component>[^:]+):(?<vendor_message>[^\n\r]+)(?<vendor_details>(?(?=^\d{4}(?:\-\d\d){2})|[^\r])+)?

Pour votre culture, à partir de vendor_details, j’utilise 3 structures qu’on ne voit pas souvent en regex (enfin, moi en tout cas…^^) :

  • Le lookahead : qui vient tester le pattern devant la position actuelle sans consommer les caractères.
    (?=<pattern>)
  • le if : (?(<pattern>)<matching si vrai>|<matching si faux>)
  • Les non-capturing group : qui permettent de regrouper des pattern sans créer de groupe de correspondance supplémentaire.
    (?:<pattern>)

Autant vous dire que c’est pas du tout performant (néanmoins admettez que c’est joli !) comme le parseur passe son temps à regarder devant et revenir à cause du lookahead.

Challenge !

L’exemple de logs ci-dessus avec la regex que je présente prend 10 700 « step » sur regex101 pour s’exécuter. A vous de faire mieux ! Celui qui arrive à simplifier fortement la regex (i.e, la rendre linéaire, sans perdre les champs), gagnera un backlink vers son site/mail/profil youporn linkedin juste en dessous. Réponse dans les commentaire ou via le mail de contact.

Pas de vainqueur pour l’instant !

a vous de jouer!

App Splunk certbot et Let’s Encrypt

Bref, et voilà pour l’App Splunk certbot et Let’s Encrypt, je soupçonne que ce sera la dernière avant un moment, puisque je vais changer de techno de SOC avec mon nouveau job. Mes apps resteront maintenues, tant que j’utiliserai Splunk pour superviser le site. En tout cas, J’espère vous avoir appris 2-3 trucs sur le fonctionnement des applications Splunk et des expressions régulières.

D’ici à la prochaine : geekez-bien !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.