├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── ChangeLog ├── LICENSE ├── Makefile ├── README.Windows.md ├── README.md ├── clang-format ├── config ├── ngx_http_auth_spnego_module.c └── scripts └── kerberos_ldap /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | build-test: 8 | 9 | # nginx-dev doesn't exist on releases before 24.04 10 | strategy: 11 | matrix: 12 | os: 13 | - ubuntu-24.04 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | 19 | - name: Update APT package index 20 | run: | 21 | sudo apt-get update -qq 22 | 23 | - name: Install packages 24 | run: | 25 | sudo apt-get install \ 26 | nginx nginx-dev build-essential libkrb5-dev curl \ 27 | slapd ldap-utils \ 28 | krb5-admin-server krb5-kdc krb5-kdc-ldap \ 29 | libsasl2-modules-gssapi-mit \ 30 | php-fpm php-ldap 31 | 32 | - name: Check out repository code 33 | uses: actions/checkout@v4 34 | 35 | - name: Create build directory 36 | run: | 37 | mkdir "${{ github.workspace }}/build" 38 | 39 | - name: Run configure script 40 | run: | 41 | cd /usr/share/nginx/src 42 | . ./conf_flags 43 | ./configure \ 44 | --with-cc-opt="-fPIC" \ 45 | --with-ld-opt="-Wl,-z,relro" \ 46 | "${NGX_CONF_FLAGS[@]}" \ 47 | --add-dynamic-module="${{ github.workspace }}" \ 48 | --builddir="${{ github.workspace }}/build" 49 | 50 | - name: Build module 51 | run: | 52 | cd "${{ github.workspace }}/build" 53 | make \ 54 | -f "${{ github.workspace }}/build/Makefile" \ 55 | -C "/usr/share/nginx/src" \ 56 | modules 57 | 58 | - name: List files in the repository and build dir 59 | run: | 60 | echo "=== Workspace: ${{ github.workspace }} ===" 61 | ls -al "${{ github.workspace }}" 62 | echo "=== Build dir: ${{ github.workspace }}/build ===" 63 | ls -al "${{ github.workspace }}/build" 64 | 65 | - name: Install module 66 | run: | 67 | sudo mkdir -p /usr/lib/nginx/modules/ 68 | sudo cp "${{ github.workspace }}/build/ngx_http_auth_spnego_module.so" /usr/lib/nginx/modules/ 69 | sudo mkdir -p /usr/share/nginx/modules-available/ 70 | echo "load_module modules/ngx_http_auth_spnego_module.so;" >> "${{ github.workspace }}/build/mod-http-auth-spnego.conf" 71 | sudo cp "${{ github.workspace }}/build/mod-http-auth-spnego.conf" /usr/share/nginx/modules-available/ 72 | sudo mkdir -p /etc/nginx/modules-enabled/ 73 | sudo ln -sf /usr/share/nginx/modules-available/mod-http-auth-spnego.conf /etc/nginx/modules-enabled/50-mod-http-auth-spnego.conf 74 | 75 | - name: Run test script 76 | run: | 77 | sudo bash "${{ github.workspace }}/scripts/kerberos_ldap" 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.[oa] 2 | *.so 3 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Author: Sean Timothy Noonan 2 | Date: Mon Mar 16 15:23:37 2015 -0400 3 | 4 | Link to libgssapi in FreeBSD 5 | 6 | This fixes the inability of this module to compile 7 | on FreeBSD 10.1 8 | 9 | commit e59a22d3cf78fe756d7b381ad0b88dfd7fa9aad6 10 | Author: Sean Timothy Noonan 11 | Date: Tue Oct 21 21:13:12 2014 -0400 12 | 13 | Add additional log and debug statements for bogus auth 14 | 15 | commit fa728d2de80001d41f539783b21977a4aa493c65 16 | Author: Sean Timothy Noonan 17 | Date: Wed Oct 1 20:54:21 2014 -0400 18 | 19 | Describe bogus auth header in readme 20 | 21 | Closes #25 22 | 23 | commit 7292bce3d890f67288464939e0ce4c775e2337fe 24 | Merge: 02df21c b916c72 25 | Author: Sean Timothy Noonan 26 | Date: Thu Aug 21 22:01:02 2014 -0400 27 | 28 | Merge pull request #24 from neirbowj/illuminated_bogus 29 | 30 | Hint at the origin of the bogus password 31 | 32 | commit b916c7235fa1e3db669e57c89db1baae79151574 33 | Author: John W. O'Brien 34 | Date: Thu Aug 21 21:29:39 2014 -0400 35 | 36 | Hint at the origin of the bogus password 37 | 38 | commit 02df21c146858ac1a424ee525341027c18e022a6 39 | Merge: d9af45f d536d2e 40 | Author: Sean Timothy Noonan 41 | Date: Tue Aug 19 21:49:10 2014 -0400 42 | 43 | Merge branch 'gsskrb5_register_acceptor_identity' 44 | 45 | Closes #22 46 | 47 | commit d536d2e80ea47fe9425a7bddb67f61b63560ccde 48 | Author: Sean Timothy Noonan 49 | Date: Mon Aug 18 20:54:18 2014 -0400 50 | 51 | Correct all arguments and logic for keytabs 52 | 53 | Must have been three-quarters asleep when I wrote 54 | that code. kt_env for the env variable, kt for the 55 | gsskrb5 call. 56 | 57 | commit 1ecea2aa9f2c239db94af21ecabe033125e53baa 58 | Author: Sean Timothy Noonan 59 | Date: Tue Aug 12 22:01:31 2014 -0400 60 | 61 | Test using gsskrb5_register_acceptor_identity 62 | 63 | commit d9af45feb24fd63c6436c3323247ed9065f84cbc 64 | Author: Sean Timothy Noonan 65 | Date: Fri Aug 8 10:12:05 2014 -0400 66 | 67 | Check return value of putenv 68 | 69 | commit 54398b3718241107fb57d175619e62913a6e4590 70 | Author: Sean Timothy Noonan 71 | Date: Sat Jul 19 08:54:27 2014 -0400 72 | 73 | Less specific versions in readme, add help section 74 | 75 | commit 2723627ed875a722a805149e4458f1f3ddbba05e 76 | Author: Sean Timothy Noonan 77 | Date: Sat Jul 19 08:12:47 2014 -0400 78 | 79 | update ChangeLog 80 | 81 | commit e1bd59d1308f2ecccd883758ac61bcd0ba73c854 82 | Author: Sean Timothy Noonan 83 | Date: Sat Jul 19 07:57:17 2014 -0400 84 | 85 | Use strlchr and strncmp where needed 86 | 87 | commit de1924735de6c3fec862929efa9684d44c088f6e 88 | Author: Sean Timothy Noonan 89 | Date: Sat Jul 19 07:33:46 2014 -0400 90 | 91 | Convert remaining sprintf to snprintf 92 | 93 | commit 9519ab75de5b63f82fc018c91884b147bd70a670 94 | Merge: c1ce1d6 52d9c89 95 | Author: Sean Timothy Noonan 96 | Date: Fri Jul 11 09:07:15 2014 -0400 97 | 98 | Merge pull request #21 from aroth-arsoft/master 99 | 100 | fix for issue remote_user and realm #19 101 | 102 | commit 52d9c89447a27da64743e2e7e40f4ea783565163 103 | Author: Andreas Roth 104 | Date: Fri Jul 11 14:01:06 2014 +0200 105 | 106 | use ngx_strncmp instead of ngx_strcmp to ensure to compare only the 107 | valid part of the realm 108 | 109 | commit c1ce1d642ef38b59896182ef4a34418283132644 110 | Merge: 9430baa 70f4095 111 | Author: Sean Timothy Noonan 112 | Date: Tue Jun 24 21:41:06 2014 -0400 113 | 114 | Merge branch 'master' of github.com:stnoonan/spnego-http-auth-nginx-module 115 | 116 | commit 9430baa9ec1bf2d537ddbb93c166e98d3db89057 117 | Author: Sean Timothy Noonan 118 | Date: Tue Jun 24 21:40:55 2014 -0400 119 | 120 | Update ChangeLog 121 | 122 | commit 8cc8aa56e402da9057a66374040c0b8604069dec 123 | Author: Sean Timothy Noonan 124 | Date: Tue Jun 24 21:40:40 2014 -0400 125 | 126 | Add missing semicolon in spnego_log_krb5_error 127 | 128 | This resolves compilation errors when --with-debug is passed 129 | to the configure script. 130 | 131 | Closes #18 132 | 133 | commit 70f4095ce0dc9e9469e244bd1d238231c7e27b40 134 | Author: Sean Timothy Noonan 135 | Date: Wed Jun 18 20:57:32 2014 -0400 136 | 137 | Clarify the behavior of the module when realm is specified 138 | 139 | The default realm is stripped, auth_gss_format_full can be used 140 | to override this behavior. 141 | 142 | commit 34b74d3185b38720754cf677b2a8eb82682587e3 143 | Author: Sean Timothy Noonan 144 | Date: Wed Jun 18 19:44:07 2014 -0400 145 | 146 | Replace deprecated krb5_get_init_creds_opt_init() 147 | 148 | The preferred method is to use krb5_get_init_creds_opt_{alloc,free}, 149 | as it removes a static structure that is unable to be extended. 150 | 151 | With credit to MAXuk 152 | 153 | commit cf04148da10b1e4ce295f9617dcf822045bf9dbf 154 | Author: Sean Timothy Noonan 155 | Date: Wed Jun 18 19:35:44 2014 -0400 156 | 157 | Use krb5_get_error_message 158 | 159 | Contributed by planbnet. This should provide extended 160 | error messages from the underlying krb5 library when 161 | there are failures. 162 | 163 | commit 4a43f2f35749b38c36ffff7474284f91c1a59b8e 164 | Author: Sean Timothy Noonan 165 | Date: Wed Jun 18 19:06:20 2014 -0400 166 | 167 | Add dependency information to README.md 168 | 169 | Closes #16. 170 | 171 | commit 50b24e59c0925b42a0b3e60401ee3741b1d46717 172 | Author: Sean Timothy Noonan 173 | Date: Tue Apr 22 09:10:26 2014 -0400 174 | 175 | allow auth_gss directives in limit_except blocks 176 | 177 | commit 46e400cb87f3576a27731cce11c2f6f421a7cefd 178 | Author: Sean Timothy Noonan 179 | Date: Wed Oct 16 21:55:08 2013 -0400 180 | 181 | Update ChangeLog and README 182 | 183 | commit d335e081f6823276c18c1fce1cc195e57a3c83af 184 | Author: Sean Timothy Noonan 185 | Date: Wed Oct 16 21:43:55 2013 -0400 186 | 187 | Return 403 when user is unauthorized 188 | 189 | These changes disallow the continuous retries and provide for 190 | significantly better behavior when used with Chrome 191 | 192 | Fixes stnoonan/spnego-http-auth-nginx-module#9 193 | 194 | commit 52bd58009d68d9e94664ac8c77139431267e9cf6 195 | Merge: a18b188 60ab7e3 196 | Author: Sean Timothy Noonan 197 | Date: Wed Oct 16 06:15:36 2013 -0700 198 | 199 | Merge pull request #10 from MAXuk/patch-1 200 | 201 | Include com_err.h to define error_message 202 | 203 | Remove make error: "warning: implicit declaration of function 'error_message'" 204 | 205 | commit 60ab7e330f06c2dc329f1e82e99d0fb9a94cfac5 206 | Author: MAXuk 207 | Date: Wed Oct 16 16:58:26 2013 +0400 208 | 209 | Remove "warning: implicit declaration of function 'error_message'" 210 | 211 | " 212 | cc -c -pipe -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g -I /usr/local/include -I src/core -I src/event -I src/event/modules -I src/os/unix -I zlib-1.2.8 -I objs -I src/http -I src/http/modules -I src/mail -o objs/addon/spnego-http-auth-nginx-module-master/ngx_http_auth_spnego_module.o spnego-http-auth-nginx-module-master/ngx_http_auth_spnego_module.c 213 | cc1: warnings being treated as errors 214 | spnego-http-auth-nginx-module-master/ngx_http_auth_spnego_module.c: In function 'ngx_http_auth_spnego_basic': 215 | spnego-http-auth-nginx-module-master/ngx_http_auth_spnego_module.c:496: warning: implicit declaration of function 'error_message' 216 | *** [objs/addon/spnego-http-auth-nginx-module-master/ngx_http_auth_spnego_module.o] Error code 1 217 | " 218 | 219 | Remove this error where make nginx --add-module=spnego-http-auth-nginx-module-master 220 | 221 | commit a18b18801f0ea75723dd14ad912596788f8cef0d 222 | Author: Sean Timothy Noonan 223 | Date: Tue Oct 8 16:11:35 2013 -0400 224 | 225 | ChangeLog update 226 | 227 | commit 56109515cbef41fe603aaba08f3e0895e3b158f7 228 | Author: Sean Timothy Noonan 229 | Date: Tue Oct 8 16:08:16 2013 -0400 230 | 231 | Sane header behavior for multiple browsers 232 | 233 | Sending a token in a 401 causes Chrome to choke 234 | Sending multiple NEGOTIATE headers causes Firefox to loop indefinitely 235 | trying to authenticate. 236 | Added a new ctx entry to track the output token. This allows the header 237 | to be set only once. Future improvement would be to stop passing 238 | both the token and the ctx. 239 | 240 | commit 6d90271edb3179055c604ce4d588754513627b78 241 | Author: Sean Timothy Noonan 242 | Date: Tue Oct 8 12:04:50 2013 -0400 243 | 244 | Update ChangeLog/README to reflect recent changes 245 | 246 | commit 13507c52e5d6fdcdf8ff219230a45338a5847173 247 | Author: Sean Timothy Noonan 248 | Date: Tue Oct 8 12:02:39 2013 -0400 249 | 250 | Do not send token in reponse unless authenticated 251 | 252 | This causes a parsing error in Google Chrome, causing it to 253 | display a 320 instead of a 401. 254 | 255 | commit c6c51e7fd02d946a6863106fc27b054252e746a7 256 | Author: Sean Noonan 257 | Date: Mon Aug 12 17:52:53 2013 +0000 258 | 259 | pass data, not data address for auth_princs 260 | 261 | commit 827a36357998625c9a19ece717bc4c5360984695 262 | Author: Sean Timothy Noonan 263 | Date: Sat Aug 10 15:28:41 2013 -0400 264 | 265 | add size check of authorized principal 266 | 267 | The ngx_str*cmp family of functions are thin wrappers around their str*cmp 268 | equivalents. Thus, I am doing the same style check I would do with those. 269 | 270 | Removes the need for sys/param.h. Also updated the license file to have 271 | the same set of authors. 272 | 273 | commit 21eea473622bbf8aca13da88430ef68dcb5ba50a 274 | Author: Sean Timothy Noonan 275 | Date: Sat Aug 10 14:01:21 2013 -0400 276 | 277 | Correct usage of ngx_strncmp with ngx_str_t 278 | 279 | commit 4d68e56121fc8bce663ab7b84857a6f4dd2281f2 280 | Author: Sean Timothy Noonan 281 | Date: Sat Aug 10 12:51:46 2013 -0400 282 | 283 | Provide a way to disable basic auth 284 | 285 | While some of us would like to think every communication 286 | occurs over SSL, this is a pipe dream. SPNEGO can be 287 | perfectly fine over an unencrypted channel. Basic auth 288 | cannot be. Closes #6. 289 | 290 | commit 88c10bd2e72745f46c87c3f802f01a2248491abd 291 | Author: Sean Timothy Noonan 292 | Date: Sat Aug 10 12:17:00 2013 -0400 293 | 294 | Update ChangeLog 295 | 296 | commit 524f70b58e776d7471aa3fda820b7bf5011a6fff 297 | Author: Sean Timothy Noonan 298 | Date: Sat Aug 10 12:16:40 2013 -0400 299 | 300 | Correct comparison of authorized principal len 301 | 302 | Fixes #7 303 | 304 | commit ee771851afaba99ebadc616b909bc41762a852b0 305 | Author: Sean Noonan 306 | Date: Tue Jul 16 17:38:56 2013 +0000 307 | 308 | Add auth_gss_authorized_principal support 309 | 310 | This can be used to specify a list of principals that are allowed to authenticate 311 | 312 | commit 0b40e9adaf2873578ef459ba22fda502b0e4f078 313 | Author: Sean Noonan 314 | Date: Fri Jul 12 20:11:26 2013 +0000 315 | 316 | Fix compile error after haphazard NTLM addition 317 | 318 | commit 2592edb043f996bbfb3a1c8792752d6bcfb9427c 319 | Author: Sean Timothy Noonan 320 | Date: Fri Jul 12 16:01:55 2013 -0400 321 | 322 | Update README.md 323 | 324 | commit 9fb57c5b043076be41d25281bd48769f4052a3e5 325 | Author: Sean Noonan 326 | Date: Fri Jul 12 19:54:57 2013 +0000 327 | 328 | Update documentation 329 | 330 | commit 21995ec282ce04299d630e4fa87f2c9357b8328e 331 | Author: Sean Noonan 332 | Date: Fri Jul 12 19:48:27 2013 +0000 333 | 334 | Update documentation 335 | 336 | commit 81a26f9e98eb2311ba2c6e81e9adcad85b2d59be 337 | Author: Sean Noonan 338 | Date: Fri Jul 12 19:10:30 2013 +0000 339 | 340 | Use proper credentials when none are specified 341 | 342 | Rather than construct specific principal names, the correct behavior is 343 | to use GSS_C_NO_CREDENTIAL. 344 | 345 | Additionally, always send both basic auth and negotiate headers 346 | 347 | commit d6ecb02b9fe1bfb7e7eac6b98be6bc133edb9b51 348 | Author: Sean Timothy Noonan 349 | Date: Thu Jul 11 19:23:49 2013 -0400 350 | 351 | Remove hostname directive from configuration 352 | 353 | commit cc5514fd4c002cf70446f447decf806a3c3bd57d 354 | Merge: 5afe502 a00bb5d 355 | Author: Sean Timothy Noonan 356 | Date: Thu Jun 6 19:48:31 2013 -0400 357 | 358 | Merge changes due to pull request 359 | 360 | commit 5afe5024390e02cf92bbe79eb2131c0a1ad46384 361 | Author: Sean Timothy Noonan 362 | Date: Thu Jun 6 19:40:46 2013 -0400 363 | 364 | Modify README and remove TODO 365 | 366 | While the majority usage of this module will be against a Windows domain, that is certainly not the only use case. We need to be clear that the requirements for Windows are just those and do not necessarily apply across the board. 367 | 368 | commit 7f8ac65c7e2683c9a50cca52f3ecaaa738807746 369 | Author: Sean Timothy Noonan 370 | Date: Thu Jun 6 19:18:16 2013 -0400 371 | 372 | Formatting cleanup 373 | 374 | commit a00bb5d4f6c7d8dbedd9cebd8f372e7de2c5ad69 375 | Merge: 87bb358 413012a 376 | Author: Sean Timothy Noonan 377 | Date: Thu Jun 6 16:34:41 2013 -0700 378 | 379 | Merge pull request #5 from rbarrois/fix_auth_basic 380 | 381 | Fix auth basic fallback. 382 | 383 | commit 413012a2aa5bbdec4138448d0ff26546a1587b9d 384 | Author: Raphaël Barrois 385 | Date: Fri Jun 7 01:28:10 2013 +0200 386 | 387 | Properly send back WWW-Authenticate header on 401 (Closes #4). 388 | 389 | commit 5062c25c64cfb0db8eaf3acb03e70b2dc40dbf22 390 | Author: Raphaël Barrois 391 | Date: Fri Jun 7 00:27:14 2013 +0200 392 | 393 | Remove buggy calls to ngx_strlen on ngx_str_t. 394 | 395 | commit 87bb3581c4764a83517b37210f1c5119c25dc67d 396 | Merge: f6f5271 cec02d0 397 | Author: Sean Noonan 398 | Date: Thu Jun 6 20:25:33 2013 +0000 399 | 400 | Merge basic auth changes from pyhalov 401 | 402 | commit f6f5271d6d5ed9297673760e0d263ec2ab6d642d 403 | Merge: 4e8f13e 57cb988 404 | Author: Sean Timothy Noonan 405 | Date: Mon May 13 20:09:23 2013 -0700 406 | 407 | Merge pull request #2 from Roguelazer/fix_segfault 408 | 409 | fix segfault on log 410 | 411 | commit 57cb9883d011f3b251a7a18adf318ab2aae166be 412 | Author: James Brown 413 | Date: Mon May 13 19:50:48 2013 -0700 414 | 415 | fix segfault on log 416 | 417 | commit 4e8f13e16363fc90ecbd0477ec728c96d5f4a8d3 418 | Author: Sean Timothy Noonan 419 | Date: Mon May 6 11:13:16 2013 -0300 420 | 421 | Update README.md 422 | 423 | Removed Windows specific references, as they are severely misleading to people working in an MIT/Heimdal only environment. 424 | 425 | commit cec02d072286bcfdaefc1edff6f4aaf983369100 426 | Author: Alexander Pyhalov 427 | Date: Wed Apr 10 00:24:41 2013 +0400 428 | 429 | Behave like apache mod_auth_krb5: if Negotiate failed, then try pure basic. Otherwise Windows clients, which doesn't belong to domain, fail. 430 | 431 | commit b2f14e233626cda825e3fc0da115a5dbc5ed4031 432 | Author: Alexander Pyhalov 433 | Date: Mon Apr 8 17:55:49 2013 +0400 434 | 435 | Made basic authorization work. Added posibility for using GSSAPI on non-default ports. Alway pass realm to nginx if forced 436 | 437 | commit 9bd83699a325a37f17475c3dc106b46ecf86b18e 438 | Merge: df0769d f332f1c 439 | Author: Sean Timothy Noonan 440 | Date: Fri Apr 5 20:37:04 2013 -0700 441 | 442 | Merge pull request #1 from ifad/master 443 | 444 | Documentation, enhanced debugging, BOF fixes 445 | 446 | commit f332f1c53456f79fd772f017c500aeda9e173ff7 447 | Author: Marcello Barnaba 448 | Date: Tue Feb 19 18:31:37 2013 +0100 449 | 450 | README 451 | 452 | commit 6631c8aab6a94236a1470507c086be0595fb40ba 453 | Author: Marcello Barnaba 454 | Date: Tue Feb 19 14:29:36 2013 +0100 455 | 456 | Add myself to the list of authors 457 | 458 | commit af8c31d8cab62f417234f47b3365b24e2709f50a 459 | Author: Marcello Barnaba 460 | Date: Tue Feb 19 14:29:30 2013 +0100 461 | 462 | Log the input_token, to debug failures due to NTLMSSP 463 | 464 | commit af3a2444207c88d894c39eb08f4a90cbfbefda5c 465 | Author: Marcello Barnaba 466 | Date: Tue Feb 19 14:29:10 2013 +0100 467 | 468 | Silence pointer qualifier ignore warning 469 | 470 | commit 70f88935812165ce3f1d68b3bfec7214b7e469ac 471 | Author: Marcello Barnaba 472 | Date: Tue Feb 19 14:28:45 2013 +0100 473 | 474 | Build fully-qualified SPN, using host name if it is not specified in config 475 | 476 | commit b8dd3c0fbd885afb5adb93e393d4642fdc40c07a 477 | Author: Marcello Barnaba 478 | Date: Tue Feb 19 14:26:38 2013 +0100 479 | 480 | Fix another BOF 481 | 482 | commit 8b1ac33bfad41d3ad81888ff8c2a1b1de11b8676 483 | Author: Marcello Barnaba 484 | Date: Tue Feb 19 14:26:16 2013 +0100 485 | 486 | Fix buffer overflow 487 | 488 | commit 3b6b171c1cb8baf0c95379c608090acceed600bc 489 | Author: Marcello Barnaba 490 | Date: Mon Feb 18 19:13:32 2013 +0100 491 | 492 | Remove redundant -Wl option 493 | 494 | commit df0769d7341981be88bf39b82250ad6ef0edfedd 495 | Author: Sean Noonan 496 | Date: Thu Sep 27 14:37:58 2012 +0000 497 | 498 | Correct the length of the host name when there is no port specified 499 | 500 | commit 9f04ee459b469627fc92d0c573c4ae1f8933cf82 501 | Author: Sean Noonan 502 | Date: Wed Sep 26 21:43:19 2012 +0000 503 | 504 | Update README and bump version in Makefile 505 | 506 | commit 44bbdef3f10aa719fa3a150001314ff508e08f87 507 | Author: Sean Noonan 508 | Date: Wed Sep 26 21:10:52 2012 +0000 509 | 510 | remove spnegohelp 511 | 512 | commit fc2a777d4eb65d9f3bf14f3d6fac1a7d21f94a6a 513 | Author: Sean Noonan 514 | Date: Wed Sep 26 20:46:37 2012 +0000 515 | 516 | fixes GSS Negotiate fallback; reformat code for consistency 517 | 518 | commit 0cd52c0a5ac46591b0a59b117c37939ec034ac27 519 | Author: Sean Noonan 520 | Date: Wed Sep 26 18:56:35 2012 +0000 521 | 522 | NULL == 523 | 524 | commit 68ef07d74e62d04e7c07d5021c7a220a71ee63c4 525 | Author: Sean Noonan 526 | Date: Wed Sep 26 18:26:09 2012 +0000 527 | 528 | replace references to unsigned char with u_char since we have it 529 | 530 | commit 6f2a2bb8eca53d11844aa83ba50ef43968c0ce98 531 | Author: Sean Noonan 532 | Date: Wed Sep 26 17:30:44 2012 +0000 533 | 534 | Revert changes that required gss_nt_krb5_name instead of GSS_C_NT_HOSTBASED_SERVICE 535 | 536 | commit 3ed44df57ae51cf2595550acbe65ec6ecb6d2f76 537 | Author: Sean Noonan 538 | Date: Wed Sep 26 17:12:44 2012 +0000 539 | 540 | Work properly when run on non-standard ports 541 | 542 | commit 117fbd8b11b9c153fc909797cbae43407f52a754 543 | Author: Pavel Plesov 544 | Date: Wed Feb 15 17:58:35 2012 +0400 545 | 546 | Fix memory allocation in Basic auth 547 | 548 | commit c2035a7b1c900491fb3df2ea50f52fcb34f6be2f 549 | Author: Pavel Plesov 550 | Date: Wed Feb 15 17:40:20 2012 +0400 551 | 552 | Fix build and link with MIT Kerberos 1.9.2 553 | 554 | commit 6c4193f009614c047f94b68b8e1b0c2ef806836d 555 | Merge: 3cfbb2d 8ac7acf 556 | Author: Pavel Plesov 557 | Date: Wed Feb 15 18:11:41 2012 +0400 558 | 559 | Merge remote-tracking branch 'muhgatus/master' 560 | 561 | commit 3cfbb2d04d992ab69dd51f49bde9827aa4f88693 562 | Author: Pavel Plesov 563 | Date: Wed Feb 15 17:35:29 2012 +0400 564 | 565 | Fix link with local build of spnegohelp 566 | 567 | commit 06a9a5feffd8fb164c188f7ffbea40351672df3b 568 | Merge: d96a011 8c18ff7 569 | Author: Pavel Plesov 570 | Date: Wed Feb 15 18:07:35 2012 +0400 571 | 572 | Merge remote-tracking branch 'solj/master' 573 | 574 | commit d96a011d9561bd2a7845e17f4b89309d7c064d8f 575 | Author: Pavel Plesov 576 | Date: Wed Feb 15 13:54:53 2012 +0400 577 | 578 | Add FreeBSD to build configuration 579 | 580 | commit 8ac7acf97196c7f8233b88ddabbef1e132182698 581 | Author: muhgatus 582 | Date: Wed Feb 8 10:02:14 2012 +0100 583 | 584 | added basic auth via kerberos 585 | 586 | commit 8c18ff7bd857ba838ee4136663165ee6e646d001 587 | Author: Sol Jerome 588 | Date: Thu Sep 29 17:25:09 2011 -0500 589 | 590 | gitignore: Ignore non-source files 591 | 592 | Signed-off-by: Sol Jerome 593 | 594 | commit ba87108541e7b2b1e9470265b74ef93a43f811cf 595 | Author: Michael Shadle 596 | Date: Thu Jun 10 02:01:36 2010 -0700 597 | 598 | Minor typo in README 599 | 600 | commit 27ff31a356dad663dcc480f9b088520af9ba165a 601 | Author: Michael Shadle 602 | Date: Thu Jun 10 01:56:50 2010 -0700 603 | 604 | initial commit, please read README! 605 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 Michal Kowalski 3 | * Copyright (C) 2012-2013 Sean Timothy Noonan 4 | * Copyright (C) 2013 Marcello Barnaba 5 | * Copyright (C) 2013 Alexander Pyhalov 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 1. Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | * SUCH DAMAGE. 27 | * 28 | */ 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | NAME=ngx_http_auth_spnego_module 3 | VERSION=1.0.0 4 | 5 | NPKG=$(NAME)-$(VERSION) 6 | NHEAD=$(NAME)-HEAD 7 | NCURRENT=$(NAME)-current 8 | 9 | GIT-FILES:=$(shell git ls-files | grep -v ChangeLog) 10 | FILES=ChangeLog $(GIT-FILES) 11 | 12 | ChangeLog: $(GIT-FILES) 13 | git log | sed 1d> "$@" 14 | 15 | arch-release: 16 | rm -f ../$(NPKG).tar.gz ../$(NPKG).zip 17 | scripts/link-files-to .tmp/$(NPKG) $(FILES) 18 | git log > .tmp/$(NPKG)/ChangeLog 19 | tar cvzf ../$(NPKG).tar.gz -C .tmp $(NPKG) 20 | cd .tmp && zip -r ../../$(NPKG).zip $(NPKG) 21 | rm -rf .tmp 22 | 23 | arch-current: 24 | rm -f ../$(NCURRENT).tar.gz ../$(NCURRENT).zip 25 | scripts/link-files-to .tmp/$(NCURRENT) $(FILES) 26 | git log > .tmp/$(NCURRENT)/ChangeLog 27 | tar cvzf ../$(NCURRENT).tar.gz -C .tmp $(NCURRENT) 28 | cd .tmp && zip -r ../../$(NCURRENT).zip $(NCURRENT) 29 | rm -rf .tmp 30 | 31 | arch-head: 32 | rm -f ../$(NNHEAD).tar.gz ../$(NHEAD).zip 33 | git archive --format=zip --prefix=$(NHEAD)/ HEAD > ../$(NHEAD).zip 34 | git archive --format=tar --prefix=$(NHEAD)/ HEAD | gzip > ../$(NHEAD).tar.gz 35 | 36 | clean: 37 | rm -f *~ 38 | -------------------------------------------------------------------------------- /README.Windows.md: -------------------------------------------------------------------------------- 1 | Crash course to Windows KDC Configuration 2 | ========================================= 3 | 4 | On the AD side, you need to: 5 | 6 | * Create a new user, whose name should be the service name you'll be using 7 | Kerberos authentication on. E.g. `app.example`. 8 | 9 | * Set the "User cannot change password" and "Password never expires" options 10 | on the account. If you are using AES-128 or AES-256, set also the "This 11 | account supports Kerberos AES 128 bit encryption" option or the 256 bit one. 12 | It is recommended to use AES, see [here](https://blogs.technet.microsoft.com/petergu/2013/04/14/interpreting-the-supportedencryptiontypes-registry-key/) 13 | for more details. 14 | 15 | * Set a strong password on it. Feel free to forget it, you are not going to 16 | insert it anywhere. 17 | 18 | * From a Windows `cmd.exe` window, generate the service principals and keytabs 19 | for this user using [`ktpass`](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/ktpass). 20 | 21 | You need an SPN named `host/foo.example.com`, and another named 22 | `HTTP/foo.example.com`. 23 | 24 | It is **crucial** that `foo.example.com` is the DNS name of your web site in the 25 | intranet, and it is an `A` record. 26 | 27 | Given that `app.example` is the account name you created, you would execute, 28 | **in this exact order**: 29 | 30 | C:\> ktpass -princ host/foo.example.com@EXAMPLE.COM -mapuser 31 | EXAMPLECOM\app.example -pass * -out host.foo.keytab -ptype KRB5_NT_PRINCIPAL 32 | -crypto AES256-SHA1 33 | 34 | C:\> ktpass -princ HTTP/foo.example.com@EXAMPLE.COM -mapuser 35 | EXAMPLECOM\app.example -pass * -out http.foo.keytab -ptype KRB5_NT_PRINCIPAL 36 | -crypto AES256-SHA1 37 | 38 | * If you need to support multiple host names using the same service account, 39 | you can issue further `ktpass` commands, but **ensure to issue the `host/` 40 | one first and the `HTTP/` one after**. Example: 41 | 42 | C:\> ktpass -princ host/bar.example.com@EXAMPLE.COM -mapuser 43 | EXAMPLECOM\app.example -pass * -out host.bar.keytab -ptype KRB5_NT_PRINCIPAL 44 | -crypto AES256-SHA1 45 | 46 | C:\> ktpass -princ HTTP/bar.example.com@EXAMPLE.COM -mapuser 47 | EXAMPLECOM\app.example -pass * -out http.bar.keytab -ptype KRB5_NT_PRINCIPAL 48 | -crypto AES256-SHA1 49 | 50 | * Verify that the correct SPNs are created using [`setspn`](https://social.technet.microsoft.com/wiki/contents/articles/717.service-principal-names-spns-setspn-syntax-setspn-exe.aspx) 51 | 52 | C:\> setspn -Q */foo.example.com 53 | 54 | it should yield both the `HTTP/` and `host/` SPNs, both mapped to the 55 | `app.example` user. Example with a service accessible via both 56 | `foo.example.com` and `bar.example.com`: 57 | 58 | C:\Temp>setspn -Q */foo.example.com 59 | Checking domain DC=example,DC=com 60 | CN=app.example,OU=Service Accounts,DC=example,DC=com 61 | HTTP/bar.example.com 62 | host/bar.example.com 63 | HTTP/foo.example.com 64 | host/bar.example.com 65 | 66 | `setspn` lists the entries in the reverse order they were created. So **it is crucial 67 | that the HTTP and host entries are in pairs, in that order**. 68 | 69 | 70 | Crash course to UNIX KRB5 and nginx configuration 71 | ------------------------------------------------- 72 | 73 | * Verify that your UNIX machine is using the same DNS server as your DC, most 74 | likely it'll use the DC itself. 75 | 76 | * Create an `/etc/krb5.conf` configuration file, replacing the realm and kdc 77 | host names with your own AD setup: 78 | 79 | [libdefaults] 80 | default_tgs_enctypes = aes256-cts-hmac-sha1-96 81 | default_tkt_enctypes = aes256-cts-hmac-sha1-96 82 | default_keytab_name = FILE:/etc/krb5.keytab 83 | default_realm = EXAMPLE.COM 84 | ticket_lifetime = 24h 85 | kdc_timesync = 1 86 | ccache_type = 4 87 | forwardable = false 88 | proxiable = false 89 | 90 | [realms] 91 | EXAMPLE.COM = { 92 | kdc = dc.example.com 93 | admin_server = dc.example.com 94 | default_domain = example.com 95 | } 96 | 97 | [domain_realm] 98 | .kerberos.server = EXAMPLE.COM 99 | .example.com = EXAMPLE.COM 100 | 101 | * Copy the keytab files (`host.foo.keytab`, `http.foo.keytab`, etc) created 102 | with `ktpass` on Windows to your UNIX machine. 103 | 104 | * Create a `krb5.keytab` using `ktutil`, concatenating together the keytabs 105 | generated by `ktpass`: 106 | 107 | # ktutil 108 | ktutil: rkt host.foo.keytab 109 | ktutil: rkt http.foo.keytab 110 | ktutil: rkt host.bar.keytab 111 | ktutil: rkt http.bar.keytab 112 | ktutil: wkt /etc/krb5.keytab 113 | ktutil: quit 114 | 115 | * Verify that the created keytab file has been built correctly: 116 | 117 | # klist -kt /etc/krb5.keytab 118 | Keytab name: FILE:/etc/krb5.keytab 119 | KVNO Timestamp Principal 120 | ---- ----------------- -------------------------------------------------------- 121 | 1 02/19/13 04:02:48 host/foo.example.com@EXAMPLE.COM 122 | 2 02/19/13 04:02:48 HTTP/foo.example.com@EXAMPLE.COM 123 | 3 02/19/13 04:02:48 host/bar.example.com@EXAMPLE.COM 124 | 4 02/19/13 04:02:48 HTTP/bar.example.com@EXAMPLE.COM 125 | 126 | Key version numbers (`KVNO`) will be different in your case. 127 | 128 | 129 | * Verify that you are able to authenticate using the keytab, without password: 130 | 131 | # kinit -5 -V -k -t /etc/krb5.keytab HTTP/bar.example.com 132 | Authenticated to Kerberos v5 133 | 134 | # klist 135 | Ticket cache: FILE:/tmp/krb5cc_0 136 | Default principal: HTTP/foo.example.com@EXAMPLE.COM 137 | 138 | Valid starting Expires Service principal 139 | 02/19/13 17:37:42 02/20/13 03:37:40 krbtgt/EXAMPLE.COM@EXAMPLE.COM 140 | renew until 02/20/13 17:37:42 141 | 142 | Please note that you will be able to authenticate **only** with the **LAST** 143 | SPN that has been configured via `ktpass`, as windows user accounts keep the 144 | mapping only with a single SPN, and that is the one displayed in the "User 145 | logon name" field in the "Account" pane of an user account details screen of 146 | Active Directory Users and Computers, or the first result of `setspn -Q`. 147 | 148 | * Make the keytab file readable only by root and the nginx group: 149 | 150 | # chmod 440 /etc/krb5.keytab 151 | # chown root:nginx /etc/krb5.keytab 152 | 153 | * Configure a SPNEGO-protected location in the nginx configuration file: 154 | 155 | server { 156 | server_name foo.example.com; 157 | 158 | ssl on; 159 | ssl_certificate example.com.crt; 160 | ssl_certificate_key example.com.crt; 161 | 162 | location / { 163 | root /some/where; 164 | index index.html; 165 | auth_gss on; 166 | auth_gss_realm EXAMPLE.COM; 167 | auth_gss_keytab /etc/krb5.keytab; 168 | auth_gss_service_name HTTP; 169 | } 170 | } 171 | 172 | The SPN will be built as follows: 173 | 174 | $auth_gss_service_name / $server_name @ $auth_gss_realm 175 | 176 | In the above example, it'll be `HTTP/foo.example.com@EXAMPLE.COM`. You can 177 | specify a fully-qualified SPN in the `auth_gss_service_name` configuration 178 | option, in this case the `server_name` won't be added automatically. 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Nginx module for HTTP SPNEGO auth 2 | ================================= 3 | 4 | This module implements adds [SPNEGO](http://tools.ietf.org/html/rfc4178) 5 | support to nginx(http://nginx.org). It currently supports only Kerberos 6 | authentication via [GSSAPI](http://en.wikipedia.org/wiki/GSSAPI) 7 | 8 | Prerequisites 9 | ------------- 10 | 11 | Authentication has been tested with (at least) the following: 12 | 13 | * Nginx 1.2 through 1.15 14 | * Internet Explorer 8 and above 15 | * Firefox 10 and above 16 | * Chrome 20 and above 17 | * Curl 7.x (GSS-Negotiate), 7.x (SPNEGO/fbopenssl) 18 | 19 | The underlying kerberos library used for these tests was MIT KRB5 v1.12. 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | 1. Download [nginx source](http://www.nginx.org/en/download.html) 26 | 1. Extract to a directory 27 | 1. Clone this module into the directory 28 | 1. Follow the [nginx install documentation](http://nginx.org/en/docs/install.html) 29 | and pass an `--add-module` option to nginx configure: 30 | 31 | ./configure --add-module=spnego-http-auth-nginx-module 32 | 33 | Note that if it isn't clear, you do need KRB5 (MIT or Heimdal) header files installed. On Debian based distributions, including Ubuntu, this is the krb5-multidev, libkrb5-dev, heimdal-dev, or heimdal-multidev package depending on your environment. On other Linux distributions, you want the development libraries that provide gssapi_krb5.h. 34 | 35 | Configuration reference 36 | ----------------------- 37 | 38 | You can configure GSS authentication on a per-location and/or a global basis: 39 | 40 | These options are required. 41 | * `auth_gss`: on/off, for ease of unsecuring while leaving other options in 42 | the config file 43 | * `auth_gss_keytab`: absolute path-name to keytab file containing service 44 | credentials 45 | 46 | These options should ONLY be specified if you have a keytab containing 47 | privileged principals. In nearly all cases, you should not put these 48 | in the configuration file, as `gss_accept_sec_context` will do the right 49 | thing. 50 | * `auth_gss_realm`: Kerberos realm name. If this is specified, the realm is only passed to the nginx variable $remote_user if it differs from this default. To override this behavior, set *auth_gss_format_full* to `on` in your configuration. 51 | * `auth_gss_service_name`: service principal name to use when acquiring 52 | credentials. 53 | 54 | If you would like to authorize only a specific set of principals, you can use the 55 | `auth_gss_authorized_principal` directive. The configuration syntax supports 56 | multiple entries, one per line. 57 | 58 | auth_gss_authorized_principal @ 59 | auth_gss_authorized_principal @ 60 | 61 | Principals can also be authorized using a regex pattern via the `auth_gss_authorized_principal_regex` 62 | directive. This directive can be used together with the `auth_gss_authorized_principal` directive. 63 | 64 | auth_gss_authorized_principal @ 65 | auth_gss_authorized_principal_regex ^()/()@$ 66 | 67 | The remote user header in nginx can only be set by doing basic authentication. 68 | Thus, this module sets a bogus basic auth header that will reach your backend 69 | application in order to set this header/nginx variable. The easiest way to disable 70 | this behavior is to add the following configuration to your location config. 71 | 72 | proxy_set_header Authorization ""; 73 | 74 | A future version of the module may make this behavior an option, but this should 75 | be a sufficient workaround for now. 76 | 77 | If you would like to enable GSS local name rules to rewrite usernames, you can 78 | specify the `auth_gss_map_to_local` option. 79 | 80 | Credential Delegation 81 | ----------------------------- 82 | 83 | User credentials can be delegated to nginx using the `auth_gss_delegate_credentials` 84 | directive. This directive will enable unconstrained delegation if the user chooses 85 | to delegate their credentials. Constrained delegation (S4U2proxy) can also be enabled using the 86 | `auth_gss_constrained_delegation` directive together with the `auth_gss_delegate_credentials` 87 | directive. To specify the ccache file name to store the service ticket used for constrained 88 | delegation, set the `auth_gss_service_ccache` directive. Otherwise, the default ccache name 89 | will be used. 90 | 91 | auth_gss_service_ccache /tmp/krb5cc_0; 92 | auth_gss_delegate_credentials on; 93 | auth_gss_constrained_delegation on; 94 | 95 | The delegated credentials will be stored within the systems tmp directory. Once the 96 | request is completed, the credentials file will be destroyed. The name of the credentials 97 | file will be specified within the nginx variable `$krb5_cc_name`. Usage of the variable 98 | can include passing it to a fcgi program using the `fastcgi_param` directive. 99 | 100 | fastcgi_param KRB5CCNAME $krb5_cc_name; 101 | 102 | Constrained delegation is currently only supported using the negotiate authentication scheme 103 | and has only been testing with MIT Kerberos (Use at your own risk if using Heimdal Kerberos). 104 | 105 | Basic authentication fallback 106 | ----------------------------- 107 | 108 | The module falls back to basic authentication by default if no negotiation is 109 | attempted by the client. If you are using SPNEGO without SSL, it is recommended 110 | you disable basic authentication fallback, as the password would be sent in 111 | plaintext. This is done by setting `auth_gss_allow_basic_fallback` in the 112 | config file. 113 | 114 | auth_gss_allow_basic_fallback off 115 | 116 | These options affect the operation of basic authentication: 117 | * `auth_gss_realm`: Kerberos realm name. If this is specified, the realm is 118 | only passed to the nginx variable $remote_user if it differs from this 119 | default. To override this behavior, set *auth_gss_format_full* to 1 in your 120 | configuration. 121 | * `auth_gss_force_realm`: Forcibly authenticate using the realm configured in 122 | `auth_gss_realm` or the system default realm if `auth_gss_realm` is not set. 123 | This will rewrite $remote_user if the client provided a different realm. If 124 | *auth_gss_format_full* is not set, $remote_user will not include a realm even 125 | if one was specified by the client. 126 | 127 | 128 | Troubleshooting 129 | --------------- 130 | 131 | ### 132 | Check the logs. If you see a mention of NTLM, your client is attempting to 133 | connect using [NTLMSSP](http://en.wikipedia.org/wiki/NTLMSSP), which is 134 | unsupported and insecure. 135 | 136 | ### Verify that you have an HTTP principal in your keytab ### 137 | 138 | #### MIT Kerberos utilities #### 139 | 140 | $ KRB5_KTNAME=FILE: klist -k 141 | 142 | or 143 | 144 | $ ktutil 145 | ktutil: read_kt 146 | ktutil: list 147 | 148 | #### Heimdal Kerberos utilities #### 149 | 150 | $ ktutil -k list 151 | 152 | ### Obtain an HTTP principal 153 | 154 | If you find that you do not have the HTTP service principal, 155 | are running in an Active Directory environment, 156 | and are bound to the domain such that Samba tools work properly 157 | 158 | $ env KRB5_KTNAME=FILE: net ads -P keytab add HTTP 159 | 160 | If you are running in a different kerberos environment, you can likely run 161 | 162 | $ env KRB5_KTNAME=FILE: krb5_keytab HTTP 163 | 164 | ### Increase maximum allowed header size 165 | 166 | In Active Directory environment, SPNEGO token in the Authorization header includes 167 | PAC (Privilege Access Certificate) information, which includes all security groups 168 | the user belongs to. This may cause the header to grow beyond default 8kB limit and 169 | causes following error message: 170 | 171 | 400 Bad Request 172 | Request Header Or Cookie Too Large 173 | 174 | For performance reasons, best solution is to reduce the number of groups the user 175 | belongs to. When this is impractical, you may also choose to increase the allowed 176 | header size by explicitly setting the number and size of Nginx header buffers: 177 | 178 | large_client_header_buffers 8 32k; 179 | 180 | Debugging 181 | --------- 182 | 183 | The module prints all sort of debugging information if nginx is compiled with 184 | the `--with-debug` option, and the `error_log` directive has a `debug` level. 185 | 186 | 187 | NTLM 188 | ---- 189 | 190 | Note that the module does not support [NTLMSSP](http://en.wikipedia.org/wiki/NTLMSSP) 191 | in Negotiate. NTLM, both v1 and v2, is an exploitable protocol and should be avoided 192 | where possible. 193 | 194 | 195 | Windows 196 | ------- 197 | 198 | For Windows KDC/AD environments, see the documentation [here](README.Windows.md). 199 | 200 | 201 | Help 202 | ---- 203 | 204 | If you're unable to figure things out, please feel free to open an 205 | issue on Github and I'll do my best to help you. 206 | -------------------------------------------------------------------------------- /clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignArrayOfStructures: None 7 | AlignConsecutiveMacros: None 8 | AlignConsecutiveAssignments: None 9 | AlignConsecutiveBitFields: None 10 | AlignConsecutiveDeclarations: None 11 | AlignEscapedNewlines: Right 12 | AlignOperands: Align 13 | AlignTrailingComments: true 14 | AllowAllArgumentsOnNextLine: true 15 | AllowAllConstructorInitializersOnNextLine: true 16 | AllowAllParametersOfDeclarationOnNextLine: true 17 | AllowShortEnumsOnASingleLine: true 18 | AllowShortBlocksOnASingleLine: Never 19 | AllowShortCaseLabelsOnASingleLine: false 20 | AllowShortFunctionsOnASingleLine: All 21 | AllowShortLambdasOnASingleLine: All 22 | AllowShortIfStatementsOnASingleLine: Never 23 | AllowShortLoopsOnASingleLine: false 24 | AlwaysBreakAfterDefinitionReturnType: None 25 | AlwaysBreakAfterReturnType: None 26 | AlwaysBreakBeforeMultilineStrings: false 27 | AlwaysBreakTemplateDeclarations: MultiLine 28 | AttributeMacros: 29 | - __capability 30 | BinPackArguments: true 31 | BinPackParameters: true 32 | BraceWrapping: 33 | AfterCaseLabel: false 34 | AfterClass: false 35 | AfterControlStatement: Never 36 | AfterEnum: false 37 | AfterFunction: false 38 | AfterNamespace: false 39 | AfterObjCDeclaration: false 40 | AfterStruct: false 41 | AfterUnion: false 42 | AfterExternBlock: false 43 | BeforeCatch: false 44 | BeforeElse: false 45 | BeforeLambdaBody: false 46 | BeforeWhile: false 47 | IndentBraces: false 48 | SplitEmptyFunction: true 49 | SplitEmptyRecord: true 50 | SplitEmptyNamespace: true 51 | BreakBeforeBinaryOperators: None 52 | BreakBeforeConceptDeclarations: true 53 | BreakBeforeBraces: Attach 54 | BreakBeforeInheritanceComma: false 55 | BreakInheritanceList: BeforeColon 56 | BreakBeforeTernaryOperators: true 57 | BreakConstructorInitializersBeforeComma: false 58 | BreakConstructorInitializers: BeforeColon 59 | BreakAfterJavaFieldAnnotations: false 60 | BreakStringLiterals: true 61 | ColumnLimit: 80 62 | CommentPragmas: '^ IWYU pragma:' 63 | CompactNamespaces: false 64 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 65 | ConstructorInitializerIndentWidth: 4 66 | ContinuationIndentWidth: 4 67 | Cpp11BracedListStyle: true 68 | DeriveLineEnding: true 69 | DerivePointerAlignment: false 70 | DisableFormat: false 71 | EmptyLineAfterAccessModifier: Never 72 | EmptyLineBeforeAccessModifier: LogicalBlock 73 | ExperimentalAutoDetectBinPacking: false 74 | FixNamespaceComments: true 75 | ForEachMacros: 76 | - foreach 77 | - Q_FOREACH 78 | - BOOST_FOREACH 79 | IfMacros: 80 | - KJ_IF_MAYBE 81 | IncludeBlocks: Preserve 82 | IncludeCategories: 83 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 84 | Priority: 2 85 | SortPriority: 0 86 | CaseSensitive: false 87 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 88 | Priority: 3 89 | SortPriority: 0 90 | CaseSensitive: false 91 | - Regex: '.*' 92 | Priority: 1 93 | SortPriority: 0 94 | CaseSensitive: false 95 | IncludeIsMainRegex: '(Test)?$' 96 | IncludeIsMainSourceRegex: '' 97 | IndentAccessModifiers: false 98 | IndentCaseLabels: false 99 | IndentCaseBlocks: false 100 | IndentGotoLabels: true 101 | IndentPPDirectives: None 102 | IndentExternBlock: AfterExternBlock 103 | IndentRequires: false 104 | IndentWidth: 4 105 | IndentWrappedFunctionNames: false 106 | InsertTrailingCommas: None 107 | JavaScriptQuotes: Leave 108 | JavaScriptWrapImports: true 109 | KeepEmptyLinesAtTheStartOfBlocks: true 110 | LambdaBodyIndentation: Signature 111 | MacroBlockBegin: '' 112 | MacroBlockEnd: '' 113 | MaxEmptyLinesToKeep: 1 114 | NamespaceIndentation: None 115 | ObjCBinPackProtocolList: Auto 116 | ObjCBlockIndentWidth: 2 117 | ObjCBreakBeforeNestedBlockParam: true 118 | ObjCSpaceAfterProperty: false 119 | ObjCSpaceBeforeProtocolList: true 120 | PenaltyBreakAssignment: 2 121 | PenaltyBreakBeforeFirstCallParameter: 19 122 | PenaltyBreakComment: 300 123 | PenaltyBreakFirstLessLess: 120 124 | PenaltyBreakString: 1000 125 | PenaltyBreakTemplateDeclaration: 10 126 | PenaltyExcessCharacter: 1000000 127 | PenaltyReturnTypeOnItsOwnLine: 60 128 | PenaltyIndentedWhitespace: 0 129 | PointerAlignment: Right 130 | PPIndentWidth: -1 131 | ReferenceAlignment: Pointer 132 | ReflowComments: true 133 | ShortNamespaceLines: 1 134 | SortIncludes: CaseSensitive 135 | SortJavaStaticImport: Before 136 | SortUsingDeclarations: true 137 | SpaceAfterCStyleCast: false 138 | SpaceAfterLogicalNot: false 139 | SpaceAfterTemplateKeyword: true 140 | SpaceBeforeAssignmentOperators: true 141 | SpaceBeforeCaseColon: false 142 | SpaceBeforeCpp11BracedList: false 143 | SpaceBeforeCtorInitializerColon: true 144 | SpaceBeforeInheritanceColon: true 145 | SpaceBeforeParens: ControlStatements 146 | SpaceAroundPointerQualifiers: Default 147 | SpaceBeforeRangeBasedForLoopColon: true 148 | SpaceInEmptyBlock: false 149 | SpaceInEmptyParentheses: false 150 | SpacesBeforeTrailingComments: 1 151 | SpacesInAngles: Never 152 | SpacesInConditionalStatement: false 153 | SpacesInContainerLiterals: true 154 | SpacesInCStyleCastParentheses: false 155 | SpacesInLineCommentPrefix: 156 | Minimum: 1 157 | Maximum: -1 158 | SpacesInParentheses: false 159 | SpacesInSquareBrackets: false 160 | SpaceBeforeSquareBrackets: false 161 | BitFieldColonSpacing: Both 162 | Standard: Latest 163 | StatementAttributeLikeMacros: 164 | - Q_EMIT 165 | StatementMacros: 166 | - Q_UNUSED 167 | - QT_REQUIRE_VERSION 168 | TabWidth: 8 169 | UseCRLF: false 170 | UseTab: Never 171 | WhitespaceSensitiveMacros: 172 | - STRINGIZE 173 | - PP_STRINGIZE 174 | - BOOST_PP_STRINGIZE 175 | - NS_SWIFT_NAME 176 | - CF_SWIFT_NAME 177 | ... 178 | 179 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_auth_spnego_module 2 | ngx_feature_libs="-lgssapi_krb5 -lkrb5 -lcom_err" 3 | 4 | if uname -o | grep -q FreeBSD; then 5 | ngx_feature_libs="$ngx_feature_libs -lgssapi" 6 | fi 7 | 8 | if uname -a | grep -q NetBSD; then 9 | ngx_feature_libs="-lgssapi -lkrb5 -lcom_err" 10 | fi 11 | 12 | if test -n "$ngx_module_link"; then 13 | ngx_module_type=HTTP 14 | ngx_module_name=ngx_http_auth_spnego_module 15 | ngx_module_srcs="$ngx_addon_dir/ngx_http_auth_spnego_module.c" 16 | ngx_module_libs="$ngx_feature_libs" 17 | . auto/module 18 | else 19 | HTTP_MODULES="$HTTP_MODULES ngx_http_auth_spnego_module" 20 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_auth_spnego_module.c" 21 | CORE_LIBS="$CORE_LIBS $ngx_feature_libs" 22 | fi 23 | -------------------------------------------------------------------------------- /ngx_http_auth_spnego_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 Michal Kowalski 3 | * Copyright (C) 2012-2013 Sean Timothy Noonan 4 | * Copyright (C) 2013 Marcello Barnaba 5 | * Copyright (C) 2013 Alexander Pyhalov 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 1. Redistributions of source code must retain the above copyright 11 | * notice, this list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | * SUCH DAMAGE. 27 | * 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #define CCACHE_VARIABLE_NAME "krb5_cc_name" 40 | #define SHM_ZONE_NAME "shm_zone" 41 | #define RENEWAL_TIME 60 42 | 43 | #define discard_const(ptr) ((void *)((uintptr_t)(ptr))) 44 | 45 | #define spnego_log_krb5_error(context, code) \ 46 | { \ 47 | const char *___kerror = krb5_get_error_message(context, code); \ 48 | spnego_debug2("Kerberos error: %d, %s", code, ___kerror); \ 49 | krb5_free_error_message(context, ___kerror); \ 50 | } 51 | #define spnego_error(code) \ 52 | ret = code; \ 53 | goto end 54 | #define spnego_debug0(msg) \ 55 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, msg) 56 | #define spnego_debug1(msg, one) \ 57 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, msg, one) 58 | #define spnego_debug2(msg, one, two) \ 59 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, msg, one, two) 60 | #define spnego_debug3(msg, one, two, three) \ 61 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, msg, one, two, \ 62 | three) 63 | #define spnego_log_error(fmt, args...) \ 64 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, fmt, ##args) 65 | 66 | #ifndef krb5_realm_length 67 | #define krb5_realm_length(r) ((r).length) 68 | #define krb5_realm_data(r) ((r).data) 69 | #endif 70 | 71 | /* Module handler */ 72 | static ngx_int_t ngx_http_auth_spnego_handler(ngx_http_request_t *); 73 | 74 | static void *ngx_http_auth_spnego_create_loc_conf(ngx_conf_t *); 75 | static char *ngx_http_auth_spnego_merge_loc_conf(ngx_conf_t *, void *, void *); 76 | static ngx_int_t ngx_http_auth_spnego_init(ngx_conf_t *); 77 | 78 | #if (NGX_PCRE) 79 | static char *ngx_conf_set_regex_array_slot(ngx_conf_t *cf, ngx_command_t *cmd, 80 | void *conf); 81 | #endif 82 | 83 | ngx_int_t ngx_http_auth_spnego_set_bogus_authorization(ngx_http_request_t *r); 84 | 85 | const char *get_gss_error(ngx_pool_t *p, OM_uint32 error_status, char *prefix) { 86 | OM_uint32 maj_stat, min_stat; 87 | OM_uint32 msg_ctx = 0; 88 | gss_buffer_desc status_string; 89 | char buf[1024]; 90 | size_t len; 91 | ngx_str_t str; 92 | ngx_snprintf((u_char *)buf, sizeof(buf), "%s: %Z", prefix); 93 | len = ngx_strlen(buf); 94 | do { 95 | maj_stat = gss_display_status(&min_stat, error_status, GSS_C_MECH_CODE, 96 | GSS_C_NO_OID, &msg_ctx, &status_string); 97 | if (sizeof(buf) > len + status_string.length + 1) { 98 | ngx_sprintf((u_char *)buf + len, "%s:%Z", 99 | (char *)status_string.value); 100 | len += (status_string.length + 1); 101 | } 102 | gss_release_buffer(&min_stat, &status_string); 103 | } while (!GSS_ERROR(maj_stat) && msg_ctx != 0); 104 | 105 | str.len = len + 1; /* "include" '\0' */ 106 | str.data = (u_char *)buf; 107 | return (char *)(ngx_pstrdup(p, &str)); 108 | } 109 | 110 | static ngx_shm_zone_t *shm_zone; 111 | 112 | typedef enum { TYPE_KRB5_CREDS, TYPE_GSS_CRED_ID_T } creds_type; 113 | 114 | typedef struct { 115 | void *data; 116 | creds_type type; 117 | } creds_info; 118 | 119 | /* per request/connection */ 120 | typedef struct { 121 | ngx_str_t token; /* decoded Negotiate token */ 122 | ngx_int_t head; /* non-zero flag if headers set */ 123 | ngx_int_t ret; /* current return code */ 124 | ngx_str_t token_out_b64; /* base64 encoded output tokent */ 125 | } ngx_http_auth_spnego_ctx_t; 126 | 127 | typedef struct { 128 | ngx_flag_t protect; 129 | ngx_str_t realm; 130 | ngx_str_t keytab; 131 | ngx_str_t service_ccache; 132 | ngx_str_t srvcname; 133 | ngx_str_t shm_zone_name; 134 | ngx_flag_t fqun; 135 | ngx_flag_t force_realm; 136 | ngx_flag_t allow_basic; 137 | ngx_array_t *auth_princs; 138 | #if (NGX_PCRE) 139 | ngx_array_t *auth_princs_regex; 140 | #endif 141 | ngx_flag_t map_to_local; 142 | ngx_flag_t delegate_credentials; 143 | ngx_flag_t constrained_delegation; 144 | } ngx_http_auth_spnego_loc_conf_t; 145 | 146 | #define SPNEGO_NGX_CONF_FLAGS \ 147 | NGX_HTTP_MAIN_CONF \ 148 | | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG 149 | 150 | /* Module Directives */ 151 | static ngx_command_t ngx_http_auth_spnego_commands[] = { 152 | {ngx_string("auth_gss"), SPNEGO_NGX_CONF_FLAGS, ngx_conf_set_flag_slot, 153 | NGX_HTTP_LOC_CONF_OFFSET, 154 | offsetof(ngx_http_auth_spnego_loc_conf_t, protect), NULL}, 155 | 156 | {ngx_string("auth_gss_zone_name"), NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, 157 | ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, 158 | offsetof(ngx_http_auth_spnego_loc_conf_t, shm_zone_name), NULL}, 159 | 160 | {ngx_string("auth_gss_realm"), SPNEGO_NGX_CONF_FLAGS, ngx_conf_set_str_slot, 161 | NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_auth_spnego_loc_conf_t, realm), 162 | NULL}, 163 | 164 | {ngx_string("auth_gss_keytab"), SPNEGO_NGX_CONF_FLAGS, 165 | ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, 166 | offsetof(ngx_http_auth_spnego_loc_conf_t, keytab), NULL}, 167 | 168 | {ngx_string("auth_gss_service_ccache"), SPNEGO_NGX_CONF_FLAGS, 169 | ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, 170 | offsetof(ngx_http_auth_spnego_loc_conf_t, service_ccache), NULL}, 171 | 172 | {ngx_string("auth_gss_service_name"), SPNEGO_NGX_CONF_FLAGS, 173 | ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, 174 | offsetof(ngx_http_auth_spnego_loc_conf_t, srvcname), NULL}, 175 | 176 | {ngx_string("auth_gss_format_full"), SPNEGO_NGX_CONF_FLAGS, 177 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 178 | offsetof(ngx_http_auth_spnego_loc_conf_t, fqun), NULL}, 179 | 180 | {ngx_string("auth_gss_force_realm"), SPNEGO_NGX_CONF_FLAGS, 181 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 182 | offsetof(ngx_http_auth_spnego_loc_conf_t, force_realm), NULL}, 183 | 184 | {ngx_string("auth_gss_allow_basic_fallback"), SPNEGO_NGX_CONF_FLAGS, 185 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 186 | offsetof(ngx_http_auth_spnego_loc_conf_t, allow_basic), NULL}, 187 | 188 | {ngx_string("auth_gss_authorized_principal"), 189 | SPNEGO_NGX_CONF_FLAGS | NGX_CONF_1MORE, ngx_conf_set_str_array_slot, 190 | NGX_HTTP_LOC_CONF_OFFSET, 191 | offsetof(ngx_http_auth_spnego_loc_conf_t, auth_princs), NULL}, 192 | #if (NGX_PCRE) 193 | {ngx_string("auth_gss_authorized_principal_regex"), 194 | SPNEGO_NGX_CONF_FLAGS | NGX_CONF_1MORE, ngx_conf_set_regex_array_slot, 195 | NGX_HTTP_LOC_CONF_OFFSET, 196 | offsetof(ngx_http_auth_spnego_loc_conf_t, auth_princs_regex), NULL}, 197 | #endif 198 | {ngx_string("auth_gss_map_to_local"), SPNEGO_NGX_CONF_FLAGS, 199 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 200 | offsetof(ngx_http_auth_spnego_loc_conf_t, map_to_local), NULL}, 201 | 202 | {ngx_string("auth_gss_delegate_credentials"), SPNEGO_NGX_CONF_FLAGS, 203 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 204 | offsetof(ngx_http_auth_spnego_loc_conf_t, delegate_credentials), NULL}, 205 | 206 | {ngx_string("auth_gss_constrained_delegation"), SPNEGO_NGX_CONF_FLAGS, 207 | ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 208 | offsetof(ngx_http_auth_spnego_loc_conf_t, constrained_delegation), NULL}, 209 | 210 | ngx_null_command}; 211 | 212 | /* Module Context */ 213 | static ngx_http_module_t ngx_http_auth_spnego_module_ctx = { 214 | NULL, /* preconf */ 215 | ngx_http_auth_spnego_init, /* postconf */ 216 | NULL, /* create main conf (defaults) */ 217 | NULL, /* init main conf (what's in nginx.conf) */ 218 | NULL, /* create server conf */ 219 | NULL, /* merge with main */ 220 | 221 | ngx_http_auth_spnego_create_loc_conf, /* create location conf */ 222 | ngx_http_auth_spnego_merge_loc_conf, /* merge with server */ 223 | }; 224 | 225 | /* Module Definition */ 226 | ngx_module_t ngx_http_auth_spnego_module = { 227 | /* ngx_uint_t ctx_index, index, spare{0-3}, version; */ 228 | NGX_MODULE_V1, /* 0, 0, 0, 0, 0, 0, 1 */ 229 | &ngx_http_auth_spnego_module_ctx, /* void *ctx */ 230 | ngx_http_auth_spnego_commands, /* ngx_command_t *commands */ 231 | NGX_HTTP_MODULE, /* ngx_uint_t type = 0x50545448 */ 232 | NULL, /* ngx_int_t (*init_master)(ngx_log_t *log) */ 233 | NULL, /* ngx_int_t (*init_module)(ngx_cycle_t *cycle) */ 234 | NULL, /* ngx_int_t (*init_process)(ngx_cycle_t *cycle) */ 235 | NULL, /* ngx_int_t (*init_thread)(ngx_cycle_t *cycle) */ 236 | NULL, /* void (*exit_thread)(ngx_cycle_t *cycle) */ 237 | NULL, /* void (*exit_process)(ngx_cycle_t *cycle) */ 238 | NULL, /* void (*exit_master)(ngx_cycle_t *cycle) */ 239 | NGX_MODULE_V1_PADDING, /* 0, 0, 0, 0, 0, 0, 0, 0 */ 240 | /* uintptr_t spare_hook{0-7}; */ 241 | }; 242 | 243 | #if (NGX_PCRE) 244 | static char *ngx_conf_set_regex_array_slot(ngx_conf_t *cf, ngx_command_t *cmd, 245 | void *conf) { 246 | char *p = conf; 247 | u_char errstr[NGX_MAX_CONF_ERRSTR]; 248 | ngx_str_t *value; 249 | ngx_regex_elt_t *re; 250 | ngx_regex_compile_t rc; 251 | ngx_array_t **a; 252 | ngx_conf_post_t *post; 253 | 254 | a = (ngx_array_t **)(p + cmd->offset); 255 | 256 | if (*a == NGX_CONF_UNSET_PTR) { 257 | *a = ngx_array_create(cf->pool, 4, sizeof(ngx_regex_elt_t)); 258 | if (*a == NULL) { 259 | return NGX_CONF_ERROR; 260 | } 261 | } 262 | 263 | re = ngx_array_push(*a); 264 | if (re == NULL) { 265 | return NGX_CONF_ERROR; 266 | } 267 | 268 | value = cf->args->elts; 269 | 270 | ngx_memzero(&rc, sizeof(ngx_regex_compile_t)); 271 | 272 | rc.pattern = value[1]; 273 | rc.pool = cf->pool; 274 | rc.err.len = NGX_MAX_CONF_ERRSTR; 275 | rc.err.data = errstr; 276 | 277 | if (ngx_regex_compile(&rc) != NGX_OK) { 278 | return NGX_CONF_ERROR; 279 | } 280 | 281 | re->regex = rc.regex; 282 | re->name = value[1].data; 283 | 284 | if (cmd->post) { 285 | post = cmd->post; 286 | return post->post_handler(cf, post, re); 287 | } 288 | 289 | return NGX_CONF_OK; 290 | } 291 | #endif 292 | 293 | static void *ngx_http_auth_spnego_create_loc_conf(ngx_conf_t *cf) { 294 | ngx_http_auth_spnego_loc_conf_t *conf; 295 | 296 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_spnego_loc_conf_t)); 297 | if (NULL == conf) { 298 | return NGX_CONF_ERROR; 299 | } 300 | 301 | conf->protect = NGX_CONF_UNSET; 302 | conf->fqun = NGX_CONF_UNSET; 303 | conf->force_realm = NGX_CONF_UNSET; 304 | conf->allow_basic = NGX_CONF_UNSET; 305 | conf->auth_princs = NGX_CONF_UNSET_PTR; 306 | #if (NGX_PCRE) 307 | conf->auth_princs_regex = NGX_CONF_UNSET_PTR; 308 | #endif 309 | conf->map_to_local = NGX_CONF_UNSET; 310 | conf->delegate_credentials = NGX_CONF_UNSET; 311 | conf->constrained_delegation = NGX_CONF_UNSET; 312 | 313 | return conf; 314 | } 315 | 316 | static ngx_int_t ngx_http_auth_spnego_init_shm_zone(ngx_shm_zone_t *shm_zone, 317 | void *data) { 318 | if (data) { 319 | shm_zone->data = data; 320 | return NGX_OK; 321 | } 322 | 323 | shm_zone->data = shm_zone->shm.addr; 324 | return NGX_OK; 325 | } 326 | 327 | static ngx_int_t ngx_http_auth_spnego_create_shm_zone(ngx_conf_t *cf, 328 | ngx_str_t *name) { 329 | if (shm_zone != NULL) return NGX_OK; 330 | 331 | shm_zone = 332 | ngx_shared_memory_add(cf, name, 65536, &ngx_http_auth_spnego_module); 333 | if (shm_zone == NULL) { 334 | return NGX_ERROR; 335 | } 336 | 337 | shm_zone->init = ngx_http_auth_spnego_init_shm_zone; 338 | 339 | return NGX_OK; 340 | } 341 | 342 | static char *ngx_http_auth_spnego_merge_loc_conf(ngx_conf_t *cf, void *parent, 343 | void *child) { 344 | ngx_http_auth_spnego_loc_conf_t *prev = parent; 345 | ngx_http_auth_spnego_loc_conf_t *conf = child; 346 | 347 | /* "off" by default */ 348 | ngx_conf_merge_off_value(conf->protect, prev->protect, 0); 349 | ngx_conf_merge_str_value(conf->shm_zone_name, prev->shm_zone_name, SHM_ZONE_NAME); 350 | 351 | if (conf->protect != 0) { 352 | if (ngx_http_auth_spnego_create_shm_zone(cf, &conf->shm_zone_name) != NGX_OK) { 353 | ngx_conf_log_error(NGX_LOG_INFO, cf, 0, 354 | "auth_spnego: failed to create shared memory zone"); 355 | return NGX_CONF_ERROR; 356 | } 357 | } 358 | 359 | ngx_conf_merge_str_value(conf->realm, prev->realm, ""); 360 | ngx_conf_merge_str_value(conf->keytab, prev->keytab, "/etc/krb5.keytab"); 361 | 362 | ngx_conf_merge_str_value(conf->service_ccache, prev->service_ccache, ""); 363 | 364 | ngx_conf_merge_str_value(conf->srvcname, prev->srvcname, ""); 365 | 366 | ngx_conf_merge_off_value(conf->fqun, prev->fqun, 0); 367 | ngx_conf_merge_off_value(conf->force_realm, prev->force_realm, 0); 368 | ngx_conf_merge_off_value(conf->allow_basic, prev->allow_basic, 1); 369 | 370 | ngx_conf_merge_ptr_value(conf->auth_princs, prev->auth_princs, 371 | NGX_CONF_UNSET_PTR); 372 | 373 | #if (NGX_PCRE) 374 | ngx_conf_merge_ptr_value(conf->auth_princs_regex, prev->auth_princs_regex, 375 | NGX_CONF_UNSET_PTR); 376 | #endif 377 | 378 | ngx_conf_merge_off_value(conf->map_to_local, prev->map_to_local, 0); 379 | 380 | ngx_conf_merge_off_value(conf->delegate_credentials, 381 | prev->delegate_credentials, 0); 382 | ngx_conf_merge_off_value(conf->constrained_delegation, 383 | prev->constrained_delegation, 0); 384 | 385 | #if (NGX_DEBUG) 386 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: protect = %i", 387 | conf->protect); 388 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: realm@0x%p = %s", 389 | conf->realm.data, conf->realm.data); 390 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: keytab@0x%p = %s", 391 | conf->keytab.data, conf->keytab.data); 392 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 393 | "auth_spnego: service_ccache@0x%p = %s", 394 | conf->service_ccache.data, conf->service_ccache.data); 395 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: srvcname@0x%p = %s", 396 | conf->srvcname.data, conf->srvcname.data); 397 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: fqun = %i", 398 | conf->fqun); 399 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: allow_basic = %i", 400 | conf->allow_basic); 401 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: force_realm = %i", 402 | conf->force_realm); 403 | 404 | if (NGX_CONF_UNSET_PTR != conf->auth_princs) { 405 | size_t ii = 0; 406 | ngx_str_t *auth_princs = conf->auth_princs->elts; 407 | for (; ii < conf->auth_princs->nelts; ++ii) { 408 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 409 | "auth_spnego: auth_princs = %.*s", 410 | auth_princs[ii].len, auth_princs[ii].data); 411 | } 412 | } 413 | 414 | #if (NGX_PCRE) 415 | if (NGX_CONF_UNSET_PTR != conf->auth_princs_regex) { 416 | size_t ii = 0; 417 | ngx_regex_elt_t *auth_princs_regex = conf->auth_princs_regex->elts; 418 | for (; ii < conf->auth_princs_regex->nelts; ++ii) { 419 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 420 | "auth_spnego: auth_princs_regex = %.*s", 421 | ngx_strlen(auth_princs_regex[ii].name), 422 | auth_princs_regex[ii].name); 423 | } 424 | } 425 | #endif 426 | 427 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, "auth_spnego: map_to_local = %i", 428 | conf->map_to_local); 429 | 430 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 431 | "auth_spnego: delegate_credentials = %i", 432 | conf->delegate_credentials); 433 | 434 | ngx_conf_log_error(NGX_LOG_DEBUG, cf, 0, 435 | "auth_spnego: constrained_delegation = %i", 436 | conf->constrained_delegation); 437 | #endif 438 | 439 | return NGX_CONF_OK; 440 | } 441 | 442 | static ngx_int_t ngx_http_auth_spnego_get_handler(ngx_http_request_t *r, 443 | ngx_http_variable_value_t *v, 444 | uintptr_t data) { 445 | return NGX_OK; 446 | } 447 | 448 | static ngx_int_t ngx_http_auth_spnego_set_variable(ngx_http_request_t *r, 449 | ngx_str_t *name, 450 | ngx_str_t *value) { 451 | u_char *lowercase = ngx_palloc(r->pool, name->len); 452 | 453 | ngx_uint_t key = ngx_hash_strlow(lowercase, name->data, name->len); 454 | ngx_pfree(r->pool, lowercase); 455 | 456 | ngx_http_variable_value_t *v = ngx_http_get_variable(r, name, key); 457 | 458 | if (v == NULL) { 459 | return NGX_ERROR; 460 | } 461 | 462 | v->len = value->len; 463 | v->data = value->data; 464 | 465 | return NGX_OK; 466 | } 467 | 468 | static ngx_int_t ngx_http_auth_spnego_add_variable(ngx_conf_t *cf, 469 | ngx_str_t *name) { 470 | ngx_http_variable_t *var = 471 | ngx_http_add_variable(cf, name, NGX_HTTP_VAR_NOCACHEABLE); 472 | 473 | if (var == NULL) { 474 | return NGX_ERROR; 475 | } 476 | 477 | var->get_handler = ngx_http_auth_spnego_get_handler; 478 | var->data = 0; 479 | 480 | return NGX_OK; 481 | } 482 | 483 | 484 | static ngx_int_t ngx_http_auth_spnego_init(ngx_conf_t *cf) { 485 | ngx_http_handler_pt *h; 486 | ngx_http_core_main_conf_t *cmcf; 487 | 488 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); 489 | 490 | h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); 491 | if (NULL == h) { 492 | return NGX_ERROR; 493 | } 494 | 495 | *h = ngx_http_auth_spnego_handler; 496 | 497 | ngx_str_t var_name = ngx_string(CCACHE_VARIABLE_NAME); 498 | if (ngx_http_auth_spnego_add_variable(cf, &var_name) != NGX_OK) { 499 | return NGX_ERROR; 500 | } 501 | 502 | return NGX_OK; 503 | } 504 | 505 | static ngx_int_t 506 | ngx_http_auth_spnego_headers_basic_only(ngx_http_request_t *r, 507 | ngx_http_auth_spnego_ctx_t *ctx, 508 | ngx_http_auth_spnego_loc_conf_t *alcf) { 509 | ngx_str_t value = ngx_null_string; 510 | value.len = sizeof("Basic realm=\"\"") - 1 + alcf->realm.len; 511 | value.data = ngx_pcalloc(r->pool, value.len); 512 | if (NULL == value.data) { 513 | return NGX_ERROR; 514 | } 515 | ngx_snprintf(value.data, value.len, "Basic realm=\"%V\"", &alcf->realm); 516 | r->headers_out.www_authenticate = ngx_list_push(&r->headers_out.headers); 517 | if (NULL == r->headers_out.www_authenticate) { 518 | return NGX_ERROR; 519 | } 520 | 521 | r->headers_out.www_authenticate->hash = 1; 522 | #if defined(nginx_version) && nginx_version >= 1023000 523 | r->headers_out.www_authenticate->next = NULL; 524 | #endif 525 | r->headers_out.www_authenticate->key.len = sizeof("WWW-Authenticate") - 1; 526 | r->headers_out.www_authenticate->key.data = (u_char *)"WWW-Authenticate"; 527 | r->headers_out.www_authenticate->value.len = value.len; 528 | r->headers_out.www_authenticate->value.data = value.data; 529 | 530 | ctx->head = 1; 531 | 532 | return NGX_OK; 533 | } 534 | 535 | static ngx_int_t 536 | ngx_http_auth_spnego_headers(ngx_http_request_t *r, 537 | ngx_http_auth_spnego_ctx_t *ctx, ngx_str_t *token, 538 | ngx_http_auth_spnego_loc_conf_t *alcf) { 539 | ngx_str_t value = ngx_null_string; 540 | /* only use token if authorized as there appears to be a bug in 541 | * Google Chrome when parsing a 401 Negotiate with a token */ 542 | if (NULL == token || ctx->ret != NGX_OK) { 543 | value.len = sizeof("Negotiate") - 1; 544 | value.data = (u_char *)"Negotiate"; 545 | } else { 546 | value.len = 547 | sizeof("Negotiate") + token->len; /* space accounts for \0 */ 548 | value.data = ngx_pcalloc(r->pool, value.len); 549 | if (NULL == value.data) { 550 | return NGX_ERROR; 551 | } 552 | ngx_snprintf(value.data, value.len, "Negotiate %V", token); 553 | } 554 | 555 | r->headers_out.www_authenticate = ngx_list_push(&r->headers_out.headers); 556 | if (NULL == r->headers_out.www_authenticate) { 557 | return NGX_ERROR; 558 | } 559 | 560 | r->headers_out.www_authenticate->hash = 1; 561 | #if defined(nginx_version) && nginx_version >= 1023000 562 | r->headers_out.www_authenticate->next = NULL; 563 | #endif 564 | r->headers_out.www_authenticate->key.len = sizeof("WWW-Authenticate") - 1; 565 | r->headers_out.www_authenticate->key.data = (u_char *)"WWW-Authenticate"; 566 | r->headers_out.www_authenticate->value.len = value.len; 567 | r->headers_out.www_authenticate->value.data = value.data; 568 | 569 | if (alcf->allow_basic) { 570 | ngx_str_t value2 = ngx_null_string; 571 | value2.len = sizeof("Basic realm=\"\"") - 1 + alcf->realm.len; 572 | value2.data = ngx_pcalloc(r->pool, value2.len); 573 | if (NULL == value2.data) { 574 | return NGX_ERROR; 575 | } 576 | ngx_snprintf(value2.data, value2.len, "Basic realm=\"%V\"", 577 | &alcf->realm); 578 | r->headers_out.www_authenticate = 579 | ngx_list_push(&r->headers_out.headers); 580 | if (NULL == r->headers_out.www_authenticate) { 581 | return NGX_ERROR; 582 | } 583 | 584 | r->headers_out.www_authenticate->hash = 2; 585 | #if defined(nginx_version) && nginx_version >= 1023000 586 | r->headers_out.www_authenticate->next = NULL; 587 | #endif 588 | r->headers_out.www_authenticate->key.len = 589 | sizeof("WWW-Authenticate") - 1; 590 | r->headers_out.www_authenticate->key.data = 591 | (u_char *)"WWW-Authenticate"; 592 | r->headers_out.www_authenticate->value.len = value2.len; 593 | r->headers_out.www_authenticate->value.data = value2.data; 594 | } 595 | 596 | ctx->head = 1; 597 | 598 | return NGX_OK; 599 | } 600 | 601 | static bool 602 | ngx_spnego_authorized_principal(ngx_http_request_t *r, ngx_str_t *princ, 603 | ngx_http_auth_spnego_loc_conf_t *alcf) { 604 | if (NGX_CONF_UNSET_PTR == alcf->auth_princs 605 | #if (NGX_PCRE) 606 | && NGX_CONF_UNSET_PTR == alcf->auth_princs_regex 607 | #endif 608 | ) { 609 | return true; 610 | } 611 | 612 | if (NGX_CONF_UNSET_PTR != alcf->auth_princs) { 613 | spnego_debug1("Testing against %d auth princs", 614 | alcf->auth_princs->nelts); 615 | 616 | ngx_str_t *auth_princs = alcf->auth_princs->elts; 617 | size_t i = 0; 618 | for (; i < alcf->auth_princs->nelts; ++i) { 619 | if (auth_princs[i].len != princ->len) { 620 | continue; 621 | } 622 | if (ngx_strncmp(auth_princs[i].data, princ->data, princ->len) == 623 | 0) { 624 | spnego_debug2("Authorized user %.*s", princ->len, princ->data); 625 | return true; 626 | } 627 | } 628 | } 629 | #if (NGX_PCRE) 630 | if (NGX_CONF_UNSET_PTR != alcf->auth_princs_regex) { 631 | spnego_debug1("Testing against %d auth princs regex", 632 | alcf->auth_princs_regex->nelts); 633 | 634 | if (ngx_regex_exec_array(alcf->auth_princs_regex, princ, 635 | r->connection->log) == NGX_OK) { 636 | return true; 637 | } 638 | } 639 | #endif 640 | 641 | return false; 642 | } 643 | 644 | ngx_int_t ngx_http_auth_spnego_token(ngx_http_request_t *r, 645 | ngx_http_auth_spnego_ctx_t *ctx) { 646 | ngx_str_t token; 647 | ngx_str_t decoded; 648 | size_t nego_sz = sizeof("Negotiate"); 649 | 650 | if (NULL == r->headers_in.authorization) { 651 | return NGX_DECLINED; 652 | } 653 | 654 | /* but don't decode second time? */ 655 | if (ctx->token.len) 656 | return NGX_OK; 657 | 658 | token = r->headers_in.authorization->value; 659 | 660 | if (token.len < nego_sz || 661 | ngx_strncasecmp(token.data, (u_char *)"Negotiate ", nego_sz) != 0) { 662 | if (ngx_strncasecmp(token.data, (u_char *)"NTLM", sizeof("NTLM")) == 663 | 0) { 664 | spnego_log_error("Detected unsupported mechanism: NTLM"); 665 | } 666 | return NGX_DECLINED; 667 | } 668 | 669 | token.len -= nego_sz; 670 | token.data += nego_sz; 671 | 672 | while (token.len && token.data[0] == ' ') { 673 | token.len--; 674 | token.data++; 675 | } 676 | 677 | if (token.len == 0) { 678 | return NGX_DECLINED; 679 | } 680 | 681 | decoded.len = ngx_base64_decoded_length(token.len); 682 | decoded.data = ngx_pnalloc(r->pool, decoded.len); 683 | if (NULL == decoded.data) { 684 | return NGX_ERROR; 685 | } 686 | 687 | if (ngx_decode_base64(&decoded, &token) != NGX_OK) { 688 | return NGX_DECLINED; 689 | } 690 | 691 | ctx->token.len = decoded.len; 692 | ctx->token.data = decoded.data; 693 | spnego_debug2("Token decoded: %*s", token.len, token.data); 694 | 695 | return NGX_OK; 696 | } 697 | 698 | static krb5_error_code ngx_http_auth_spnego_store_krb5_creds( 699 | ngx_http_request_t *r, krb5_context kcontext, krb5_principal principal, 700 | krb5_ccache ccache, krb5_creds creds) { 701 | krb5_error_code kerr = 0; 702 | 703 | if ((kerr = krb5_cc_initialize(kcontext, ccache, principal))) { 704 | spnego_log_error("Kerberos error: Cannot initialize ccache"); 705 | spnego_log_krb5_error(kcontext, kerr); 706 | return kerr; 707 | } 708 | 709 | if ((kerr = krb5_cc_store_cred(kcontext, ccache, &creds))) { 710 | spnego_log_error("Kerberos error: Cannot store credentials"); 711 | spnego_log_krb5_error(kcontext, kerr); 712 | return kerr; 713 | } 714 | 715 | return kerr; 716 | } 717 | 718 | static krb5_error_code ngx_http_auth_spnego_store_gss_creds( 719 | ngx_http_request_t *r, krb5_context kcontext, krb5_principal principal, 720 | krb5_ccache ccache, gss_cred_id_t creds) { 721 | OM_uint32 major_status, minor_status; 722 | krb5_error_code kerr = 0; 723 | 724 | if ((kerr = krb5_cc_initialize(kcontext, ccache, principal))) { 725 | spnego_log_error("Kerberos error: Cannot initialize ccache"); 726 | spnego_log_krb5_error(kcontext, kerr); 727 | return kerr; 728 | } 729 | 730 | major_status = gss_krb5_copy_ccache(&minor_status, creds, ccache); 731 | if (GSS_ERROR(major_status)) { 732 | const char *gss_error = 733 | get_gss_error(r->pool, minor_status, 734 | "ngx_http_auth_spnego_store_gss_creds() failed"); 735 | spnego_log_error("%s", gss_error); 736 | spnego_debug1("%s", gss_error); 737 | return KRB5_CC_WRITE; 738 | } 739 | 740 | return kerr; 741 | } 742 | 743 | static void ngx_http_auth_spnego_krb5_destroy_ccache(void *data) { 744 | krb5_context kcontext; 745 | krb5_ccache ccache; 746 | krb5_error_code kerr = 0; 747 | 748 | char *ccname = (char *)data; 749 | 750 | if ((kerr = krb5_init_context(&kcontext))) { 751 | goto done; 752 | } 753 | 754 | if ((kerr = krb5_cc_resolve(kcontext, ccname, &ccache))) { 755 | goto done; 756 | } 757 | 758 | krb5_cc_destroy(kcontext, ccache); 759 | done: 760 | if (kcontext) 761 | krb5_free_context(kcontext); 762 | } 763 | 764 | static char *ngx_http_auth_spnego_replace(ngx_http_request_t *r, char *str, 765 | char find, char replace) { 766 | char *result = (char *)ngx_palloc(r->pool, ngx_strlen(str) + 1); 767 | ngx_memcpy(result, str, ngx_strlen(str) + 1); 768 | 769 | char *index = NULL; 770 | while ((index = ngx_strchr(result, find)) != NULL) { 771 | *index = replace; 772 | } 773 | return result; 774 | } 775 | 776 | static ngx_int_t 777 | ngx_http_auth_spnego_store_delegated_creds(ngx_http_request_t *r, 778 | ngx_str_t *principal_name, 779 | creds_info delegated_creds) { 780 | krb5_context kcontext = NULL; 781 | krb5_principal principal = NULL; 782 | krb5_ccache ccache = NULL; 783 | krb5_error_code kerr = 0; 784 | char *ccname = NULL; 785 | char *escaped = NULL; 786 | 787 | if (!delegated_creds.data) { 788 | spnego_log_error( 789 | "ngx_http_auth_spnego_store_delegated_creds() NULL credentials"); 790 | spnego_debug0( 791 | "ngx_http_auth_spnego_store_delegated_creds() NULL credentials"); 792 | goto done; 793 | } 794 | 795 | if ((kerr = krb5_init_context(&kcontext))) { 796 | spnego_log_error("Kerberos error: Cannot initialize kerberos context"); 797 | spnego_log_krb5_error(kcontext, kerr); 798 | goto done; 799 | } 800 | 801 | if ((kerr = krb5_parse_name(kcontext, (char *)principal_name->data, 802 | &principal))) { 803 | spnego_log_error("Kerberos error: Cannot parse principal %s", 804 | principal_name); 805 | spnego_log_krb5_error(kcontext, kerr); 806 | goto done; 807 | } 808 | 809 | escaped = 810 | ngx_http_auth_spnego_replace(r, (char *)principal_name->data, '/', '_'); 811 | 812 | size_t ccname_size = (ngx_strlen("FILE:") + ngx_strlen(P_tmpdir) + 813 | ngx_strlen("/") + ngx_strlen(escaped)) + 814 | 1; 815 | ccname = (char *)ngx_pcalloc(r->pool, ccname_size); 816 | if (NULL == ccname) { 817 | return NGX_ERROR; 818 | } 819 | 820 | ngx_snprintf((u_char *)ccname, ccname_size, "FILE:%s/%*s", P_tmpdir, 821 | ngx_strlen(escaped), escaped); 822 | 823 | if ((kerr = krb5_cc_resolve(kcontext, ccname, &ccache))) { 824 | spnego_log_error("Kerberos error: Cannot resolve ccache %s", ccname); 825 | spnego_log_krb5_error(kcontext, kerr); 826 | goto done; 827 | } 828 | 829 | switch (delegated_creds.type) { 830 | case TYPE_GSS_CRED_ID_T: 831 | kerr = ngx_http_auth_spnego_store_gss_creds( 832 | r, kcontext, principal, ccache, 833 | (gss_cred_id_t)delegated_creds.data); 834 | break; 835 | case TYPE_KRB5_CREDS: 836 | kerr = ngx_http_auth_spnego_store_krb5_creds( 837 | r, kcontext, principal, ccache, 838 | (*(krb5_creds *)delegated_creds.data)); 839 | break; 840 | default: 841 | kerr = KRB5KRB_ERR_GENERIC; 842 | } 843 | 844 | if (kerr) 845 | goto done; 846 | 847 | ngx_str_t var_name = ngx_string(CCACHE_VARIABLE_NAME); 848 | 849 | ngx_str_t var_value = ngx_null_string; 850 | var_value.data = (u_char *)ccname; 851 | var_value.len = ngx_strlen(ccname); 852 | 853 | ngx_http_auth_spnego_set_variable(r, &var_name, &var_value); 854 | 855 | ngx_pool_cleanup_t *cln = ngx_pool_cleanup_add(r->pool, 0); 856 | if (NULL == cln) { 857 | return NGX_ERROR; 858 | } 859 | 860 | cln->handler = ngx_http_auth_spnego_krb5_destroy_ccache; 861 | cln->data = ccname; 862 | done: 863 | if (escaped) 864 | ngx_pfree(r->pool, escaped); 865 | if (ccname) 866 | ngx_pfree(r->pool, ccname); 867 | if (principal) 868 | krb5_free_principal(kcontext, principal); 869 | if (ccache) 870 | krb5_cc_close(kcontext, ccache); 871 | if (kcontext) 872 | krb5_free_context(kcontext); 873 | 874 | return kerr ? NGX_ERROR : NGX_OK; 875 | } 876 | 877 | ngx_int_t ngx_http_auth_spnego_basic(ngx_http_request_t *r, 878 | ngx_http_auth_spnego_ctx_t *ctx, 879 | ngx_http_auth_spnego_loc_conf_t *alcf) { 880 | ngx_str_t host_name; 881 | ngx_str_t service; 882 | ngx_str_t user; 883 | user.data = NULL; 884 | ngx_str_t new_user; 885 | ngx_int_t ret = NGX_DECLINED; 886 | 887 | krb5_context kcontext = NULL; 888 | krb5_error_code code; 889 | krb5_principal client = NULL; 890 | krb5_principal server = NULL; 891 | krb5_creds creds; 892 | krb5_get_init_creds_opt *gic_options = NULL; 893 | char *name = NULL; 894 | unsigned char *p = NULL; 895 | 896 | code = krb5_init_context(&kcontext); 897 | if (code) { 898 | spnego_debug0("Kerberos error: Cannot initialize kerberos context"); 899 | return NGX_ERROR; 900 | } 901 | 902 | host_name = r->headers_in.host->value; 903 | service.len = alcf->srvcname.len + alcf->realm.len + 3; 904 | 905 | if (ngx_strchr(alcf->srvcname.data, '/')) { 906 | service.data = ngx_palloc(r->pool, service.len); 907 | if (NULL == service.data) { 908 | spnego_error(NGX_ERROR); 909 | } 910 | 911 | ngx_snprintf(service.data, service.len, "%V@%V%Z", &alcf->srvcname, 912 | &alcf->realm); 913 | } else { 914 | service.len += host_name.len; 915 | service.data = ngx_palloc(r->pool, service.len); 916 | if (NULL == service.data) { 917 | spnego_error(NGX_ERROR); 918 | } 919 | 920 | ngx_snprintf(service.data, service.len, "%V/%V@%V%Z", &alcf->srvcname, 921 | &host_name, &alcf->realm); 922 | } 923 | 924 | code = krb5_parse_name(kcontext, (const char *)service.data, &server); 925 | 926 | if (code) { 927 | spnego_log_error("Kerberos error: Unable to parse service name"); 928 | spnego_log_krb5_error(kcontext, code); 929 | spnego_error(NGX_ERROR); 930 | } 931 | 932 | code = krb5_unparse_name(kcontext, server, &name); 933 | if (code) { 934 | spnego_log_error("Kerberos error: Cannot unparse servicename"); 935 | spnego_log_krb5_error(kcontext, code); 936 | spnego_error(NGX_ERROR); 937 | } 938 | 939 | free(name); 940 | name = NULL; 941 | 942 | p = ngx_strlchr(r->headers_in.user.data, 943 | r->headers_in.user.data + r->headers_in.user.len, '@'); 944 | user.len = r->headers_in.user.len + 1; 945 | if (NULL == p) { 946 | if (alcf->force_realm && alcf->realm.len && alcf->realm.data) { 947 | user.len += alcf->realm.len + 1; /* +1 for @ */ 948 | user.data = ngx_palloc(r->pool, user.len); 949 | if (NULL == user.data) { 950 | spnego_log_error("Not enough memory"); 951 | spnego_error(NGX_ERROR); 952 | } 953 | ngx_snprintf(user.data, user.len, "%V@%V%Z", &r->headers_in.user, 954 | &alcf->realm); 955 | } else { 956 | user.data = ngx_palloc(r->pool, user.len); 957 | if (NULL == user.data) { 958 | spnego_log_error("Not enough memory"); 959 | spnego_error(NGX_ERROR); 960 | } 961 | ngx_snprintf(user.data, user.len, "%V%Z", &r->headers_in.user); 962 | } 963 | } else { 964 | if (alcf->realm.len && alcf->realm.data && 965 | ngx_strncmp(p + 1, alcf->realm.data, alcf->realm.len) == 0) { 966 | user.data = ngx_palloc(r->pool, user.len); 967 | if (NULL == user.data) { 968 | spnego_log_error("Not enough memory"); 969 | spnego_error(NGX_ERROR); 970 | } 971 | ngx_snprintf(user.data, user.len, "%V%Z", &r->headers_in.user); 972 | if (alcf->fqun == 0) { 973 | /* 974 | * Specified realm is identical to configured realm. 975 | * Truncate $remote_user to strip @REALM. 976 | */ 977 | r->headers_in.user.len -= alcf->realm.len + 1; 978 | } 979 | } else if (alcf->force_realm) { 980 | *p = '\0'; 981 | user.len = ngx_strlen(r->headers_in.user.data) + 1; 982 | if (alcf->realm.len && alcf->realm.data) 983 | user.len += alcf->realm.len + 1; 984 | user.data = ngx_pcalloc(r->pool, user.len); 985 | if (NULL == user.data) { 986 | spnego_log_error("Not enough memory"); 987 | spnego_error(NGX_ERROR); 988 | } 989 | if (alcf->realm.len && alcf->realm.data) 990 | ngx_snprintf(user.data, user.len, "%s@%V%Z", 991 | r->headers_in.user.data, &alcf->realm); 992 | else 993 | ngx_snprintf(user.data, user.len, "%s%Z", 994 | r->headers_in.user.data); 995 | /* 996 | * Rewrite $remote_user with the forced realm. 997 | * If the forced realm is shorter than the 998 | * specified realm, we can reuse the original 999 | * buffer. 1000 | */ 1001 | if (r->headers_in.user.len >= user.len - 1) 1002 | r->headers_in.user.len = user.len - 1; 1003 | else { 1004 | new_user.len = user.len - 1; 1005 | new_user.data = ngx_palloc(r->pool, new_user.len); 1006 | if (NULL == new_user.data) { 1007 | spnego_log_error("Not enough memory"); 1008 | spnego_error(NGX_ERROR); 1009 | } 1010 | ngx_pfree(r->pool, r->headers_in.user.data); 1011 | r->headers_in.user.data = new_user.data; 1012 | r->headers_in.user.len = new_user.len; 1013 | } 1014 | ngx_memcpy(r->headers_in.user.data, user.data, 1015 | r->headers_in.user.len); 1016 | } else { 1017 | user.data = ngx_palloc(r->pool, user.len); 1018 | if (NULL == user.data) { 1019 | spnego_log_error("Not enough memory"); 1020 | spnego_error(NGX_ERROR); 1021 | } 1022 | ngx_snprintf(user.data, user.len, "%V%Z", &r->headers_in.user); 1023 | } 1024 | } 1025 | 1026 | spnego_debug1("Attempting authentication with principal %s", 1027 | (const char *)user.data); 1028 | 1029 | code = krb5_parse_name(kcontext, (const char *)user.data, &client); 1030 | if (code) { 1031 | spnego_log_error("Kerberos error: Unable to parse username"); 1032 | spnego_debug1("username is %s.", (const char *)user.data); 1033 | spnego_log_krb5_error(kcontext, code); 1034 | spnego_error(NGX_ERROR); 1035 | } 1036 | 1037 | memset(&creds, 0, sizeof(creds)); 1038 | 1039 | code = krb5_unparse_name(kcontext, client, &name); 1040 | if (code) { 1041 | spnego_log_error("Kerberos error: Cannot unparse username"); 1042 | spnego_log_krb5_error(kcontext, code); 1043 | spnego_error(NGX_ERROR); 1044 | } 1045 | 1046 | krb5_get_init_creds_opt_alloc(kcontext, &gic_options); 1047 | 1048 | code = krb5_get_init_creds_password(kcontext, &creds, client, 1049 | (char *)r->headers_in.passwd.data, NULL, 1050 | NULL, 0, NULL, gic_options); 1051 | 1052 | if (code) { 1053 | spnego_log_error("Kerberos error: Credentials failed"); 1054 | spnego_log_krb5_error(kcontext, code); 1055 | spnego_error(NGX_DECLINED); 1056 | } 1057 | 1058 | if (alcf->delegate_credentials) { 1059 | creds_info delegated_creds = {&creds, TYPE_KRB5_CREDS}; 1060 | 1061 | ngx_str_t principal_name = ngx_null_string; 1062 | principal_name.data = (u_char *)name; 1063 | principal_name.len = ngx_strlen(name); 1064 | 1065 | ngx_http_auth_spnego_store_delegated_creds(r, &principal_name, 1066 | delegated_creds); 1067 | } 1068 | 1069 | krb5_free_cred_contents(kcontext, &creds); 1070 | /* Try to add the system realm to $remote_user if needed. */ 1071 | if (alcf->fqun && !ngx_strlchr(r->headers_in.user.data, 1072 | r->headers_in.user.data + r->headers_in.user.len, '@')) { 1073 | #ifdef krb5_princ_realm 1074 | /* 1075 | * MIT does not have krb5_principal_get_realm() but its 1076 | * krb5_princ_realm() is a macro that effectively points 1077 | * to a char *. 1078 | */ 1079 | const char *realm = krb5_princ_realm(kcontext, client)->data; 1080 | #else 1081 | const char *realm = krb5_principal_get_realm(kcontext, client); 1082 | #endif 1083 | if (realm) { 1084 | new_user.len = r->headers_in.user.len + 1 + ngx_strlen(realm); 1085 | new_user.data = ngx_palloc(r->pool, new_user.len); 1086 | if (NULL == new_user.data) { 1087 | spnego_log_error("Not enough memory"); 1088 | spnego_error(NGX_ERROR); 1089 | } 1090 | ngx_snprintf(new_user.data, new_user.len, "%V@%s", 1091 | &r->headers_in.user, realm); 1092 | ngx_pfree(r->pool, r->headers_in.user.data); 1093 | r->headers_in.user.data = new_user.data; 1094 | r->headers_in.user.len = new_user.len; 1095 | } 1096 | } 1097 | 1098 | spnego_debug1("Setting $remote_user to %V", &r->headers_in.user); 1099 | if (ngx_http_auth_spnego_set_bogus_authorization(r) != NGX_OK) 1100 | spnego_log_error("Failed to set $remote_user"); 1101 | 1102 | spnego_debug0("ngx_http_auth_spnego_basic: returning NGX_OK"); 1103 | 1104 | ret = NGX_OK; 1105 | 1106 | end: 1107 | if (name) 1108 | free(name); 1109 | if (client) 1110 | krb5_free_principal(kcontext, client); 1111 | if (server) 1112 | krb5_free_principal(kcontext, server); 1113 | if (service.data) 1114 | ngx_pfree(r->pool, service.data); 1115 | if (user.data) 1116 | ngx_pfree(r->pool, user.data); 1117 | 1118 | krb5_get_init_creds_opt_free(kcontext, gic_options); 1119 | 1120 | krb5_free_context(kcontext); 1121 | 1122 | return ret; 1123 | } 1124 | 1125 | /* 1126 | * Because 'remote_user' is assumed to be provided by basic authorization 1127 | * (see ngx_http_variable_remote_user) we are forced to create bogus 1128 | * non-Negotiate authorization header. This may possibly clobber Negotiate 1129 | * token too soon. 1130 | */ 1131 | ngx_int_t ngx_http_auth_spnego_set_bogus_authorization(ngx_http_request_t *r) { 1132 | const char *bogus_passwd = "bogus_auth_gss_passwd"; 1133 | ngx_str_t plain, encoded, final; 1134 | 1135 | if (r->headers_in.user.len == 0) { 1136 | spnego_debug0("ngx_http_auth_spnego_set_bogus_authorization: no user " 1137 | "NGX_DECLINED"); 1138 | return NGX_DECLINED; 1139 | } 1140 | 1141 | /* +1 because of the ":" in "user:password" */ 1142 | plain.len = r->headers_in.user.len + ngx_strlen(bogus_passwd) + 1; 1143 | plain.data = ngx_pnalloc(r->pool, plain.len); 1144 | if (NULL == plain.data) { 1145 | return NGX_ERROR; 1146 | } 1147 | 1148 | ngx_snprintf(plain.data, plain.len, "%V:%s", &r->headers_in.user, 1149 | bogus_passwd); 1150 | 1151 | encoded.len = ngx_base64_encoded_length(plain.len); 1152 | encoded.data = ngx_pnalloc(r->pool, encoded.len); 1153 | if (NULL == encoded.data) { 1154 | return NGX_ERROR; 1155 | } 1156 | 1157 | ngx_encode_base64(&encoded, &plain); 1158 | 1159 | final.len = sizeof("Basic ") + encoded.len - 1; 1160 | final.data = ngx_pnalloc(r->pool, final.len); 1161 | if (NULL == final.data) { 1162 | return NGX_ERROR; 1163 | } 1164 | 1165 | ngx_snprintf(final.data, final.len, "Basic %V", &encoded); 1166 | 1167 | /* WARNING clobbering authorization header value */ 1168 | r->headers_in.authorization->value.len = final.len; 1169 | r->headers_in.authorization->value.data = final.data; 1170 | 1171 | spnego_debug0( 1172 | "ngx_http_auth_spnego_set_bogus_authorization: bogus user set"); 1173 | return NGX_OK; 1174 | } 1175 | 1176 | static bool use_keytab(ngx_http_request_t *r, ngx_str_t *keytab) { 1177 | size_t kt_sz = keytab->len + 1; 1178 | char *kt = (char *)ngx_pcalloc(r->pool, kt_sz); 1179 | if (NULL == kt) { 1180 | return false; 1181 | } 1182 | ngx_snprintf((u_char *)kt, kt_sz, "%V%Z", keytab); 1183 | OM_uint32 major_status, minor_status = 0; 1184 | major_status = gsskrb5_register_acceptor_identity(kt); 1185 | if (GSS_ERROR(major_status)) { 1186 | spnego_log_error( 1187 | "%s failed to register keytab", 1188 | get_gss_error(r->pool, minor_status, 1189 | "gsskrb5_register_acceptor_identity() failed")); 1190 | return false; 1191 | } 1192 | 1193 | spnego_debug1("Use keytab %V", keytab); 1194 | return true; 1195 | } 1196 | 1197 | static krb5_error_code ngx_http_auth_spnego_verify_server_credentials( 1198 | ngx_http_request_t *r, krb5_context kcontext, ngx_str_t *principal_name, 1199 | krb5_ccache ccache) { 1200 | krb5_creds match_creds; 1201 | krb5_creds creds; 1202 | krb5_timestamp now; 1203 | krb5_error_code kerr = 0; 1204 | krb5_principal principal = NULL; 1205 | char *tgs_principal_name = NULL; 1206 | char *princ_name = NULL; 1207 | 1208 | memset(&match_creds, 0, sizeof(match_creds)); 1209 | memset(&creds, 0, sizeof(creds)); 1210 | 1211 | if ((kerr = krb5_cc_get_principal(kcontext, ccache, &principal))) { 1212 | spnego_log_error("Kerberos error: Cannot get principal from ccache"); 1213 | spnego_log_krb5_error(kcontext, kerr); 1214 | goto done; 1215 | } 1216 | 1217 | if ((kerr = krb5_unparse_name(kcontext, principal, &princ_name))) { 1218 | spnego_log_error("Kerberos error: Cannot unparse principal"); 1219 | spnego_log_krb5_error(kcontext, kerr); 1220 | goto done; 1221 | } 1222 | 1223 | if (ngx_strncmp(principal_name->data, princ_name, ngx_strlen(princ_name)) != 1224 | 0) { 1225 | spnego_log_error("Kerberos error: Principal name mismatch"); 1226 | spnego_debug0("Kerberos error: Principal name mismatch"); 1227 | kerr = KRB5KRB_ERR_GENERIC; 1228 | goto done; 1229 | } 1230 | 1231 | size_t tgs_principal_name_size = 1232 | (ngx_strlen(KRB5_TGS_NAME) + (krb5_realm_length(principal->realm) * 2) + 2) + 1; 1233 | tgs_principal_name = (char *)ngx_pcalloc(r->pool, tgs_principal_name_size); 1234 | ngx_snprintf((u_char *)tgs_principal_name, tgs_principal_name_size, 1235 | "%s/%*s@%*s", KRB5_TGS_NAME, krb5_realm_length(principal->realm), 1236 | krb5_realm_data(principal->realm), krb5_realm_length(principal->realm), 1237 | krb5_realm_data(principal->realm)); 1238 | 1239 | if ((kerr = krb5_parse_name(kcontext, tgs_principal_name, 1240 | &match_creds.server))) { 1241 | spnego_log_error("Kerberos error: Cannot parse principal: %s", 1242 | tgs_principal_name); 1243 | spnego_log_krb5_error(kcontext, kerr); 1244 | goto done; 1245 | } 1246 | 1247 | match_creds.client = principal; 1248 | 1249 | if ((kerr = krb5_cc_retrieve_cred(kcontext, ccache, 0, &match_creds, 1250 | &creds))) { 1251 | spnego_log_error("Kerberos error: Cannot retrieve credentials"); 1252 | spnego_log_krb5_error(kcontext, kerr); 1253 | goto done; 1254 | } 1255 | 1256 | if ((kerr = krb5_timeofday(kcontext, &now))) { 1257 | spnego_log_error("Kerberos error: Could not get current time"); 1258 | spnego_log_krb5_error(kcontext, kerr); 1259 | goto done; 1260 | } 1261 | 1262 | if ((now + RENEWAL_TIME) > creds.times.endtime) { 1263 | spnego_debug2("Credentials for %s have expired or will expire soon at " 1264 | "%d - renewing", 1265 | princ_name, creds.times.endtime); 1266 | kerr = KRB5KRB_AP_ERR_TKT_EXPIRED; 1267 | } else { 1268 | spnego_debug2("Credentials for %s will expire at %d", princ_name, 1269 | creds.times.endtime); 1270 | } 1271 | done: 1272 | if (principal) 1273 | krb5_free_principal(kcontext, principal); 1274 | if (match_creds.server) 1275 | krb5_free_principal(kcontext, match_creds.server); 1276 | if (creds.client) 1277 | krb5_free_cred_contents(kcontext, &creds); 1278 | 1279 | return kerr; 1280 | } 1281 | 1282 | static ngx_int_t ngx_http_auth_spnego_obtain_server_credentials( 1283 | ngx_http_request_t *r, ngx_str_t *service_name, ngx_str_t *keytab_path, 1284 | ngx_str_t *service_ccache) { 1285 | krb5_context kcontext = NULL; 1286 | krb5_keytab keytab = NULL; 1287 | krb5_ccache ccache = NULL; 1288 | krb5_error_code kerr = 0; 1289 | krb5_principal principal = NULL; 1290 | krb5_get_init_creds_opt gicopts; 1291 | krb5_creds creds; 1292 | #ifdef HEIMDAL_DEPRECATED 1293 | // only used to call krb5_get_init_creds_opt_alloc() in newer heimdal 1294 | krb5_get_init_creds_opt *gicopts_l; 1295 | #endif 1296 | 1297 | char *principal_name = NULL; 1298 | char *tgs_principal_name = NULL; 1299 | char kt_path[1024]; 1300 | char cc_name[1024]; 1301 | 1302 | memset(&creds, 0, sizeof(creds)); 1303 | 1304 | if ((kerr = krb5_init_context(&kcontext))) { 1305 | spnego_log_error("Kerberos error: Cannot initialize kerberos context"); 1306 | spnego_log_krb5_error(kcontext, kerr); 1307 | goto done; 1308 | } 1309 | 1310 | if (service_ccache->len && service_ccache->data) { 1311 | ngx_snprintf((u_char *)cc_name, sizeof(cc_name), "FILE:%V%Z", 1312 | service_ccache); 1313 | 1314 | if ((kerr = krb5_cc_resolve(kcontext, cc_name, &ccache))) { 1315 | spnego_log_error("Kerberos error: Cannot resolve ccache %s", 1316 | cc_name); 1317 | spnego_log_krb5_error(kcontext, kerr); 1318 | goto done; 1319 | } 1320 | } else { 1321 | if ((kerr = krb5_cc_default(kcontext, &ccache))) { 1322 | spnego_log_error("Kerberos error: Cannot get default ccache"); 1323 | spnego_log_krb5_error(kcontext, kerr); 1324 | goto done; 1325 | } 1326 | 1327 | ngx_snprintf((u_char *)cc_name, sizeof(cc_name), "%s:%s", 1328 | krb5_cc_get_type(kcontext, ccache), 1329 | krb5_cc_get_name(kcontext, ccache)); 1330 | } 1331 | 1332 | if ((kerr = ngx_http_auth_spnego_verify_server_credentials( 1333 | r, kcontext, service_name, ccache))) { 1334 | if (kerr == KRB5_FCC_NOFILE || kerr == KRB5KRB_AP_ERR_TKT_EXPIRED) { 1335 | if ((kerr = krb5_parse_name(kcontext, (char *)service_name->data, 1336 | &principal))) { 1337 | spnego_log_error("Kerberos error: Cannot parse principal %s", 1338 | (char *)service_name->data); 1339 | spnego_log_krb5_error(kcontext, kerr); 1340 | goto done; 1341 | } 1342 | if ((kerr = 1343 | krb5_unparse_name(kcontext, principal, &principal_name))) { 1344 | spnego_log_error("Kerberos error: Cannot unparse principal"); 1345 | spnego_log_krb5_error(kcontext, kerr); 1346 | goto done; 1347 | } 1348 | } else { 1349 | spnego_log_error( 1350 | "Kerberos error: Error verifying server credentials"); 1351 | spnego_log_krb5_error(kcontext, kerr); 1352 | goto done; 1353 | } 1354 | } else { 1355 | spnego_debug0("Server credentials valid"); 1356 | goto done; 1357 | } 1358 | 1359 | ngx_slab_pool_t *shpool = (ngx_slab_pool_t *)shm_zone->shm.addr; 1360 | 1361 | ngx_shmtx_lock(&shpool->mutex); 1362 | 1363 | kerr = ngx_http_auth_spnego_verify_server_credentials(r, kcontext, 1364 | service_name, ccache); 1365 | if ((kerr != KRB5_FCC_NOFILE && kerr != KRB5KRB_AP_ERR_TKT_EXPIRED)) 1366 | goto unlock; 1367 | 1368 | ngx_snprintf((u_char *)kt_path, sizeof(kt_path), "FILE:%V%Z", keytab_path); 1369 | 1370 | if ((kerr = krb5_kt_resolve(kcontext, kt_path, &keytab))) { 1371 | spnego_log_error("Kerberos error: Cannot resolve keytab %s", kt_path); 1372 | spnego_log_krb5_error(kcontext, kerr); 1373 | goto unlock; 1374 | } 1375 | 1376 | spnego_debug1("Obtaining new credentials for %s", principal_name); 1377 | 1378 | #ifndef HEIMDAL_DEPRECATED 1379 | krb5_get_init_creds_opt_init(&gicopts); 1380 | #else 1381 | gicopts_l = &gicopts; 1382 | krb5_get_init_creds_opt_alloc(kcontext, &gicopts_l); 1383 | #endif 1384 | krb5_get_init_creds_opt_set_forwardable(&gicopts, 1); 1385 | 1386 | size_t tgs_principal_name_size = 1387 | (ngx_strlen(KRB5_TGS_NAME) + ((size_t)krb5_realm_length(principal->realm) * 2) + 2) + 1; 1388 | tgs_principal_name = (char *)ngx_pcalloc(r->pool, tgs_principal_name_size); 1389 | 1390 | ngx_snprintf((u_char *)tgs_principal_name, tgs_principal_name_size, 1391 | "%s/%*s@%*s", KRB5_TGS_NAME, krb5_realm_length(principal->realm), 1392 | krb5_realm_data(principal->realm), krb5_realm_length(principal->realm), 1393 | krb5_realm_data(principal->realm)); 1394 | 1395 | kerr = krb5_get_init_creds_keytab(kcontext, &creds, principal, keytab, 0, 1396 | tgs_principal_name, &gicopts); 1397 | if (kerr) { 1398 | spnego_log_error( 1399 | "Kerberos error: Cannot obtain credentials for principal %s", 1400 | principal_name); 1401 | spnego_log_krb5_error(kcontext, kerr); 1402 | goto unlock; 1403 | } 1404 | 1405 | if ((kerr = ngx_http_auth_spnego_store_krb5_creds(r, kcontext, principal, 1406 | ccache, creds))) { 1407 | spnego_debug0("ngx_http_auth_spnego_store_krb5_creds() failed"); 1408 | goto unlock; 1409 | } 1410 | 1411 | unlock: 1412 | ngx_shmtx_unlock(&shpool->mutex); 1413 | done: 1414 | if (!kerr) { 1415 | spnego_debug0("Successfully obtained server credentials"); 1416 | setenv("KRB5CCNAME", cc_name, 1); 1417 | } else { 1418 | spnego_debug0("Failed to obtain server credentials"); 1419 | } 1420 | 1421 | if (tgs_principal_name) 1422 | ngx_pfree(r->pool, tgs_principal_name); 1423 | if (creds.client) 1424 | krb5_free_cred_contents(kcontext, &creds); 1425 | if (keytab) 1426 | krb5_kt_close(kcontext, keytab); 1427 | if (ccache) 1428 | krb5_cc_close(kcontext, ccache); 1429 | if (kcontext) 1430 | krb5_free_context(kcontext); 1431 | 1432 | return kerr ? NGX_ERROR : NGX_OK; 1433 | } 1434 | 1435 | ngx_int_t 1436 | ngx_http_auth_spnego_auth_user_gss(ngx_http_request_t *r, 1437 | ngx_http_auth_spnego_ctx_t *ctx, 1438 | ngx_http_auth_spnego_loc_conf_t *alcf) { 1439 | ngx_int_t ret = NGX_DECLINED; 1440 | u_char *pu; 1441 | ngx_str_t spnego_token = ngx_null_string; 1442 | OM_uint32 major_status, minor_status, minor_status2; 1443 | gss_buffer_desc service = GSS_C_EMPTY_BUFFER; 1444 | gss_name_t my_gss_name = GSS_C_NO_NAME; 1445 | 1446 | gss_cred_id_t my_gss_creds = GSS_C_NO_CREDENTIAL; 1447 | gss_cred_id_t delegated_creds = GSS_C_NO_CREDENTIAL; 1448 | 1449 | gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; 1450 | gss_ctx_id_t gss_context = GSS_C_NO_CONTEXT; 1451 | gss_name_t client_name = GSS_C_NO_NAME; 1452 | gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; 1453 | 1454 | if (NULL == ctx || ctx->token.len == 0) 1455 | return ret; 1456 | 1457 | spnego_debug0("GSSAPI authorizing"); 1458 | 1459 | if (!use_keytab(r, &alcf->keytab)) { 1460 | spnego_debug0("Failed to specify keytab"); 1461 | spnego_error(NGX_ERROR); 1462 | } 1463 | 1464 | if (alcf->srvcname.len > 0) { 1465 | /* if there is a specific service prinicipal set in the configuration 1466 | * file, we need to use it. Otherwise, use the default of no 1467 | * credentials 1468 | */ 1469 | service.length = alcf->srvcname.len + alcf->realm.len + 2; 1470 | service.value = ngx_palloc(r->pool, service.length); 1471 | if (NULL == service.value) { 1472 | spnego_error(NGX_ERROR); 1473 | } 1474 | ngx_snprintf(service.value, service.length, "%V@%V%Z", &alcf->srvcname, 1475 | &alcf->realm); 1476 | 1477 | spnego_debug1("Using service principal: %s", service.value); 1478 | major_status = 1479 | gss_import_name(&minor_status, &service, 1480 | (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME, &my_gss_name); 1481 | if (GSS_ERROR(major_status)) { 1482 | spnego_log_error("%s Used service principal: %s", 1483 | get_gss_error(r->pool, minor_status, 1484 | "gss_import_name() failed"), 1485 | (u_char *)service.value); 1486 | spnego_error(NGX_ERROR); 1487 | } 1488 | gss_buffer_desc human_readable_gss_name = GSS_C_EMPTY_BUFFER; 1489 | major_status = gss_display_name(&minor_status, my_gss_name, 1490 | &human_readable_gss_name, NULL); 1491 | 1492 | if (GSS_ERROR(major_status)) { 1493 | spnego_log_error("%s Used service principal: %s ", 1494 | get_gss_error(r->pool, minor_status, 1495 | "gss_display_name() failed"), 1496 | (u_char *)service.value); 1497 | } 1498 | spnego_debug1("my_gss_name %s", human_readable_gss_name.value); 1499 | 1500 | if (alcf->constrained_delegation) { 1501 | ngx_str_t service_name = ngx_null_string; 1502 | service_name.data = (u_char *)service.value; 1503 | service_name.len = service.length; 1504 | 1505 | ngx_http_auth_spnego_obtain_server_credentials( 1506 | r, &service_name, &alcf->keytab, &alcf->service_ccache); 1507 | } 1508 | 1509 | /* Obtain credentials */ 1510 | major_status = gss_acquire_cred( 1511 | &minor_status, my_gss_name, GSS_C_INDEFINITE, GSS_C_NO_OID_SET, 1512 | (alcf->constrained_delegation ? GSS_C_BOTH : GSS_C_ACCEPT), 1513 | &my_gss_creds, NULL, NULL); 1514 | 1515 | if (GSS_ERROR(major_status)) { 1516 | spnego_log_error("%s Used service principal: %s", 1517 | get_gss_error(r->pool, minor_status, 1518 | "gss_acquire_cred() failed"), 1519 | (u_char *)service.value); 1520 | spnego_error(NGX_ERROR); 1521 | } 1522 | } 1523 | 1524 | input_token.length = ctx->token.len; 1525 | input_token.value = (void *)ctx->token.data; 1526 | 1527 | major_status = gss_accept_sec_context( 1528 | &minor_status, &gss_context, my_gss_creds, &input_token, 1529 | GSS_C_NO_CHANNEL_BINDINGS, &client_name, NULL, &output_token, NULL, 1530 | NULL, &delegated_creds); 1531 | if (GSS_ERROR(major_status)) { 1532 | spnego_debug1("%s", get_gss_error(r->pool, minor_status, 1533 | "gss_accept_sec_context() failed")); 1534 | spnego_error(NGX_DECLINED); 1535 | } 1536 | 1537 | if (major_status & GSS_S_CONTINUE_NEEDED) { 1538 | spnego_debug0("only one authentication iteration allowed"); 1539 | spnego_error(NGX_DECLINED); 1540 | } 1541 | 1542 | if (output_token.length) { 1543 | spnego_token.data = (u_char *)output_token.value; 1544 | spnego_token.len = output_token.length; 1545 | 1546 | ctx->token_out_b64.len = ngx_base64_encoded_length(spnego_token.len); 1547 | ctx->token_out_b64.data = 1548 | ngx_pcalloc(r->pool, ctx->token_out_b64.len + 1); 1549 | if (NULL == ctx->token_out_b64.data) { 1550 | spnego_log_error("Not enough memory"); 1551 | gss_release_buffer(&minor_status2, &output_token); 1552 | spnego_error(NGX_ERROR); 1553 | } 1554 | ngx_encode_base64(&ctx->token_out_b64, &spnego_token); 1555 | gss_release_buffer(&minor_status2, &output_token); 1556 | } else { 1557 | ctx->token_out_b64.len = 0; 1558 | } 1559 | 1560 | /* getting user name at the other end of the request */ 1561 | major_status = 1562 | gss_display_name(&minor_status, client_name, &output_token, NULL); 1563 | if (GSS_ERROR(major_status)) { 1564 | spnego_log_error("%s", get_gss_error(r->pool, minor_status, 1565 | "gss_display_name() failed")); 1566 | spnego_error(NGX_ERROR); 1567 | } 1568 | 1569 | if (output_token.length) { 1570 | /* Apply local rules to map Kerberos Principals to short names */ 1571 | if (alcf->map_to_local) { 1572 | gss_OID mech_type = discard_const(gss_mech_krb5); 1573 | output_token = (gss_buffer_desc)GSS_C_EMPTY_BUFFER; 1574 | major_status = gss_localname(&minor_status, client_name, mech_type, 1575 | &output_token); 1576 | if (GSS_ERROR(major_status)) { 1577 | spnego_log_error("%s", get_gss_error(r->pool, minor_status, 1578 | "gss_localname() failed")); 1579 | spnego_error(NGX_ERROR); 1580 | } 1581 | } 1582 | 1583 | /* TOFIX dirty quick trick for now (no "-1" i.e. include '\0' */ 1584 | ngx_str_t user = {output_token.length, (u_char *)output_token.value}; 1585 | 1586 | r->headers_in.user.data = ngx_pstrdup(r->pool, &user); 1587 | if (NULL == r->headers_in.user.data) { 1588 | spnego_log_error("ngx_pstrdup failed to allocate"); 1589 | spnego_error(NGX_ERROR); 1590 | } 1591 | 1592 | r->headers_in.user.len = user.len; 1593 | if (alcf->fqun == 0) { 1594 | pu = ngx_strlchr(r->headers_in.user.data, 1595 | r->headers_in.user.data + r->headers_in.user.len, 1596 | '@'); 1597 | if (pu != NULL && 1598 | ngx_strncmp(pu + 1, alcf->realm.data, alcf->realm.len) == 0) { 1599 | *pu = '\0'; 1600 | r->headers_in.user.len = ngx_strlen(r->headers_in.user.data); 1601 | } 1602 | } 1603 | 1604 | /* this for the sake of ngx_http_variable_remote_user */ 1605 | if (ngx_http_auth_spnego_set_bogus_authorization(r) != NGX_OK) { 1606 | spnego_log_error("Failed to set remote_user"); 1607 | } 1608 | spnego_debug1("user is %V", &r->headers_in.user); 1609 | } 1610 | 1611 | if (alcf->delegate_credentials) { 1612 | creds_info creds = {delegated_creds, TYPE_GSS_CRED_ID_T}; 1613 | 1614 | ngx_str_t principal_name = ngx_null_string; 1615 | principal_name.data = (u_char *)output_token.value; 1616 | principal_name.len = output_token.length; 1617 | 1618 | ngx_http_auth_spnego_store_delegated_creds(r, &principal_name, creds); 1619 | } 1620 | 1621 | gss_release_buffer(&minor_status, &output_token); 1622 | 1623 | ret = NGX_OK; 1624 | goto end; 1625 | 1626 | end: 1627 | if (output_token.length) 1628 | gss_release_buffer(&minor_status, &output_token); 1629 | 1630 | if (client_name != GSS_C_NO_NAME) 1631 | gss_release_name(&minor_status, &client_name); 1632 | 1633 | if (gss_context != GSS_C_NO_CONTEXT) 1634 | gss_delete_sec_context(&minor_status, &gss_context, GSS_C_NO_BUFFER); 1635 | 1636 | if (my_gss_name != GSS_C_NO_NAME) 1637 | gss_release_name(&minor_status, &my_gss_name); 1638 | 1639 | if (my_gss_creds != GSS_C_NO_CREDENTIAL) 1640 | gss_release_cred(&minor_status, &my_gss_creds); 1641 | 1642 | if (delegated_creds != GSS_C_NO_CREDENTIAL) 1643 | gss_release_cred(&minor_status, &delegated_creds); 1644 | 1645 | return ret; 1646 | } 1647 | 1648 | static ngx_int_t ngx_http_auth_spnego_handler(ngx_http_request_t *r) { 1649 | ngx_int_t ret = NGX_DECLINED; 1650 | ngx_http_auth_spnego_ctx_t *ctx; 1651 | ngx_http_auth_spnego_loc_conf_t *alcf; 1652 | 1653 | alcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_spnego_module); 1654 | 1655 | if (alcf->protect == 0) { 1656 | return NGX_DECLINED; 1657 | } 1658 | 1659 | ctx = ngx_http_get_module_ctx(r, ngx_http_auth_spnego_module); 1660 | if (NULL == ctx) { 1661 | ctx = ngx_palloc(r->pool, sizeof(ngx_http_auth_spnego_ctx_t)); 1662 | if (NULL == ctx) { 1663 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 1664 | } 1665 | ctx->token.len = 0; 1666 | ctx->token.data = NULL; 1667 | ctx->head = 0; 1668 | ctx->ret = NGX_HTTP_UNAUTHORIZED; 1669 | ngx_http_set_ctx(r, ctx, ngx_http_auth_spnego_module); 1670 | } 1671 | 1672 | spnego_debug3("SSO auth handling IN: token.len=%d, head=%d, ret=%d", 1673 | ctx->token.len, ctx->head, ctx->ret); 1674 | 1675 | if (ctx->token.len && ctx->head) { 1676 | spnego_debug1("Found token and head, returning %d", ctx->ret); 1677 | return ctx->ret; 1678 | } 1679 | 1680 | if (NULL != r->headers_in.user.data) { 1681 | spnego_debug0("User header set"); 1682 | return NGX_OK; 1683 | } 1684 | 1685 | spnego_debug0("Begin auth"); 1686 | 1687 | if (alcf->allow_basic) { 1688 | spnego_debug0("Detect basic auth"); 1689 | ret = ngx_http_auth_basic_user(r); 1690 | if (NGX_OK == ret) { 1691 | spnego_debug0("Basic auth credentials supplied by client"); 1692 | /* If basic auth is enabled and basic creds are supplied 1693 | * attempt basic auth. If we attempt basic auth, we do 1694 | * not fall through to real SPNEGO */ 1695 | if (NGX_OK != ngx_http_auth_spnego_basic(r, ctx, alcf)) { 1696 | spnego_debug0("Basic auth failed"); 1697 | if (NGX_ERROR == 1698 | ngx_http_auth_spnego_headers_basic_only(r, ctx, alcf)) { 1699 | spnego_debug0("Error setting headers"); 1700 | return (ctx->ret = NGX_HTTP_INTERNAL_SERVER_ERROR); 1701 | } 1702 | return (ctx->ret = NGX_HTTP_UNAUTHORIZED); 1703 | } 1704 | 1705 | if (!ngx_spnego_authorized_principal(r, &r->headers_in.user, 1706 | alcf)) { 1707 | spnego_debug0("User not authorized"); 1708 | return (ctx->ret = NGX_HTTP_FORBIDDEN); 1709 | } 1710 | 1711 | spnego_debug0("Basic auth succeeded"); 1712 | return (ctx->ret = NGX_OK); 1713 | } 1714 | } 1715 | 1716 | /* Basic auth either disabled or not supplied by client */ 1717 | spnego_debug0("Detect SPNEGO token"); 1718 | ret = ngx_http_auth_spnego_token(r, ctx); 1719 | if (NGX_OK == ret) { 1720 | spnego_debug0("Client sent a reasonable Negotiate header"); 1721 | ret = ngx_http_auth_spnego_auth_user_gss(r, ctx, alcf); 1722 | if (NGX_ERROR == ret) { 1723 | spnego_debug0("GSSAPI failed"); 1724 | return (ctx->ret = NGX_HTTP_INTERNAL_SERVER_ERROR); 1725 | } 1726 | /* There are chances that client knows about Negotiate 1727 | * but doesn't support GSSAPI. We could attempt to fall 1728 | * back to basic here... */ 1729 | if (NGX_DECLINED == ret) { 1730 | spnego_debug0("GSSAPI failed"); 1731 | if (!alcf->allow_basic) { 1732 | return (ctx->ret = NGX_HTTP_FORBIDDEN); 1733 | } 1734 | if (NGX_ERROR == 1735 | ngx_http_auth_spnego_headers_basic_only(r, ctx, alcf)) { 1736 | spnego_debug0("Error setting headers"); 1737 | return (ctx->ret = NGX_HTTP_INTERNAL_SERVER_ERROR); 1738 | } 1739 | return (ctx->ret = NGX_HTTP_UNAUTHORIZED); 1740 | } 1741 | 1742 | if (!ngx_spnego_authorized_principal(r, &r->headers_in.user, alcf)) { 1743 | spnego_debug0("User not authorized"); 1744 | return (ctx->ret = NGX_HTTP_FORBIDDEN); 1745 | } 1746 | 1747 | spnego_debug0("GSSAPI auth succeeded"); 1748 | } 1749 | 1750 | ngx_str_t *token_out_b64 = NULL; 1751 | switch (ret) { 1752 | case NGX_DECLINED: /* DECLINED, but not yet FORBIDDEN */ 1753 | ctx->ret = NGX_HTTP_UNAUTHORIZED; 1754 | break; 1755 | case NGX_OK: 1756 | ctx->ret = NGX_OK; 1757 | token_out_b64 = &ctx->token_out_b64; 1758 | break; 1759 | case NGX_ERROR: 1760 | default: 1761 | ctx->ret = NGX_HTTP_INTERNAL_SERVER_ERROR; 1762 | break; 1763 | } 1764 | 1765 | if (NGX_ERROR == 1766 | ngx_http_auth_spnego_headers(r, ctx, token_out_b64, alcf)) { 1767 | spnego_debug0("Error setting headers"); 1768 | ctx->ret = NGX_HTTP_INTERNAL_SERVER_ERROR; 1769 | } 1770 | 1771 | spnego_debug3("SSO auth handling OUT: token.len=%d, head=%d, ret=%d", 1772 | ctx->token.len, ctx->head, ctx->ret); 1773 | return ctx->ret; 1774 | } 1775 | -------------------------------------------------------------------------------- /scripts/kerberos_ldap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Test that regular authentication is working, and that constrained delegation 4 | # is working. In order for this to be doable, we set up a Kerberos KDC with 5 | # a LDAP-based backend (the LDAP DB driver has support for the necessesary 6 | # hooks that enable constrained delegation, the standard MIT DB2 backend does 7 | # not). Furthermore, we use a simple PHP script to demonstrate the delegation 8 | # by connecting to the LDAP server using the client principals passed in via 9 | # the HTTP connection. 10 | # 11 | # Note that this script is written to be usable both in autopkgtest and Github 12 | # workflows (which also implies that it needs to be compatible with Debian and 13 | # Ubuntu). 14 | # 15 | # Copyright (C) 2025 David Härdeman 16 | 17 | EX=0 18 | CURL_OUTPUT="http_body" 19 | CURL_NONEGOTIATE="curl --max-time 60 --silent --fail-with-body -o ${CURL_OUTPUT}" 20 | CURL_NEGOTIATE="${CURL_NONEGOTIATE} --negotiate -u :" 21 | DOMAIN="example.com" 22 | TEST_HOST="server" 23 | TEST_HOST_FQDN="${TEST_HOST}.${DOMAIN}" 24 | LDAP_BASE_DN="$(echo "${DOMAIN}" | sed 's/\./,/;s/\([^,]\+\)/dc=\1/g')" 25 | LDAP_SERVICES_DN="ou=Services,${LDAP_BASE_DN}" 26 | LDAP_KRB_DN="ou=kerberos,${LDAP_SERVICES_DN}" 27 | LDAP_KRB_CONTAINER_DN="cn=krbContainer,${LDAP_KRB_DN}" 28 | LDAP_KDC_DN="uid=kdc,${LDAP_KRB_DN}" 29 | LDAP_KDC_PW="kdctest" 30 | LDAP_KADMIN_DN="uid=kadmin,${LDAP_KRB_DN}" 31 | LDAP_KADMIN_PW="kadmintest" 32 | LDAP_ADMIN_DN="cn=admin,${LDAP_BASE_DN}" 33 | LDAP_ADMIN_PW="test" 34 | KRB_BOB_PW="bob@BOB@123" 35 | KERBEROS_REALM="$(echo "${DOMAIN}" | tr "[:lower:]" "[:upper:]")" 36 | export LC_ALL=C 37 | export DEBIAN_FRONTEND=noninteractive 38 | 39 | 40 | die() 41 | { 42 | echo "FAILED" 43 | exit 1 44 | } 45 | 46 | 47 | if [ -n "${AUTOPKGTEST_TMP}" ]; then 48 | cd "${AUTOPKGTEST_TMP}" || exit 1 49 | elif [ -n "${GITHUB_WORKSPACE}" ]; then 50 | # Yeah, this env variable won't be passed on by sudo... 51 | cd "${GITHUB_WORKSPACE}" || exit 1 52 | fi 53 | 54 | 55 | cat <> /etc/hosts || die 82 | if ! hostnamectl hostname "${TEST_HOST_FQDN}"; then 83 | if ! echo "${TEST_HOST_FQDN}" >> /etc/hostname; then 84 | echo "FAILED (but continuing anyway)" 85 | else 86 | echo "OK" 87 | fi 88 | else 89 | echo "OK" 90 | fi 91 | 92 | 93 | printf "Reconfiguring slapd ... " 94 | if ! debconf-set-selections < /dev/null 2>&1 || die 104 | echo "OK" 105 | 106 | 107 | printf "Verifying LDAP base DN ... " 108 | CFG_DOMAIN="$(ldapsearch -x -LLL -s base -b "" namingContexts | grep namingContexts | cut -d" " -f2)" 109 | if [ "${CFG_DOMAIN}" != "${LDAP_BASE_DN}" ]; then 110 | printf "%s != %s ... " "${CFG_DOMAIN}" "${LDAP_BASE_DN}" 111 | die 112 | fi 113 | echo "${CFG_DOMAIN} ... OK" 114 | 115 | 116 | printf "Enabling LDAP logging ... " 117 | if ! ldapmodify -Q -H ldapi:/// -Y EXTERNAL > /dev/null < /dev/null || die 132 | echo "OK" 133 | 134 | 135 | printf "Creating basic Kerberos LDAP structure ... " 136 | if ! ldapadd -x -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" > /dev/null < /dev/null < /etc/krb5.conf 202 | [libdefaults] 203 | default_realm = ${KERBEROS_REALM} 204 | dns_lookup_realm = false 205 | dns_lookup_kdc = false 206 | ticket_lifetime = 24h 207 | forwardable = true 208 | proxiable = true 209 | rdns = false 210 | 211 | [realms] 212 | ${KERBEROS_REALM} = { 213 | kdc = ${TEST_HOST_FQDN} 214 | admin_server = ${TEST_HOST_FQDN} 215 | default_domain = ${DOMAIN} 216 | } 217 | EOF 218 | then 219 | die 220 | fi 221 | echo "OK" 222 | 223 | 224 | printf "Writing /etc/krb5kdc/kdc.conf ... " 225 | if ! cat < /etc/krb5kdc/kdc.conf 226 | [realms] 227 | ${KERBEROS_REALM} = { 228 | database_module = openldap_ldapconf 229 | } 230 | 231 | [dbmodules] 232 | openldap_ldapconf = { 233 | db_library = kldap 234 | 235 | ldap_kerberos_container_dn = ${LDAP_KRB_CONTAINER_DN} 236 | 237 | # if either of these is false, then the ldap_kdc_dn needs to 238 | # have write access as explained above 239 | disable_last_success = true 240 | disable_lockout = true 241 | ldap_conns_per_server = 5 242 | ldap_servers = ldapi:/// 243 | 244 | # this object needs to have read rights on 245 | # the realm container, principal container and realm sub-trees 246 | ldap_kdc_dn = "${LDAP_KDC_DN}" 247 | 248 | # this object needs to have read and write rights on 249 | # the realm container, principal container and realm sub-trees 250 | ldap_kadmind_dn = "${LDAP_KADMIN_DN}" 251 | 252 | # this file will be used to store plaintext passwords used 253 | # to connect to the LDAP server 254 | ldap_service_password_file = /etc/krb5kdc/service.keyfile 255 | 256 | # OR, comment out ldap_kdc_dn, ldap_kadmind_dn and 257 | # ldap_service_password_file above and enable the following 258 | # two lines, if you skipped the step of creating entries/users 259 | # for the Kerberos servers 260 | 261 | #ldap_kdc_sasl_mech = EXTERNAL 262 | #ldap_kadmind_sasl_mech = EXTERNAL 263 | #ldap_servers = ldapi:/// 264 | } 265 | EOF 266 | then 267 | die 268 | fi 269 | echo "OK" 270 | 271 | 272 | printf "Writing /etc/krb5kdc/kadm5.acl ... " 273 | if ! cat < /etc/krb5kdc/kadm5.acl 274 | */admin@${KERBEROS_REALM} * 275 | EOF 276 | then 277 | die 278 | fi 279 | echo "OK" 280 | 281 | 282 | # This will create two new entries in the LDAP DIT: 283 | # ${LDAP_KRB_CONTAINER_DN} 284 | # cn=${KERBEROS_REALM},${LDAP_KRB_CONTAINER_DN} 285 | # e.g.: 286 | # cn=krbContainer,ou=kerberos,ou=Services,dc=example,dc=com 287 | # cn=EXAMPLE.COM,cn=krbContainer,ou=kerberos,ou=Services,dc=example,dc=com 288 | printf "Creating Kerberos realm %s ... " "${KERBEROS_REALM}" 289 | kdb5_ldap_util -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" -P kdcmasterpw \ 290 | create -subtrees "${LDAP_BASE_DN}" -r "${KERBEROS_REALM}" -s -H ldapi:/// \ 291 | > /dev/null || die 292 | echo "OK" 293 | 294 | 295 | printf "Stashing KDC password ... " 296 | printf "%s\n%s\n" "${LDAP_KDC_PW}" "${LDAP_KDC_PW}" | kdb5_ldap_util \ 297 | -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" \ 298 | stashsrvpw -f /etc/krb5kdc/service.keyfile \ 299 | "${LDAP_KDC_DN}" \ 300 | > /dev/null || die 301 | echo "OK" 302 | 303 | 304 | printf "Stashing kadmin password ... " 305 | printf "%s\n%s\n" "${LDAP_KADMIN_PW}" "${LDAP_KADMIN_PW}" | kdb5_ldap_util \ 306 | -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" \ 307 | stashsrvpw -f /etc/krb5kdc/service.keyfile \ 308 | "${LDAP_KADMIN_DN}" \ 309 | > /dev/null || die 310 | echo "OK" 311 | 312 | 313 | printf "Restarting KDC ... " 314 | invoke-rc.d krb5-kdc restart || die 315 | echo "OK" 316 | 317 | 318 | printf "Restarting kadmind ... " 319 | invoke-rc.d krb5-admin-server restart || die 320 | echo "OK" 321 | 322 | 323 | printf "Creating default Kerberos password policy ... " 324 | kadmin.local -q "addpol -minlength 1 defaultpol" > /dev/null || die 325 | echo "OK" 326 | 327 | 328 | printf "Creating test user principals ... " 329 | kadmin.local -q "addprinc -randkey -policy defaultpol alice" > /dev/null || die 330 | kadmin.local -q "ktadd -k krb5.alice.keytab alice" > /dev/null || die 331 | kadmin.local -q "addprinc -pw ${KRB_BOB_PW} -policy defaultpol bob" > /dev/null || die 332 | kadmin.local -q "addprinc -randkey -policy defaultpol mallory" > /dev/null || die 333 | kadmin.local -q "ktadd -k krb5.mallory.keytab mallory" > /dev/null || die 334 | echo "OK" 335 | 336 | 337 | printf "Creating LDAP server principal ... " 338 | kadmin.local -q "addprinc -randkey -policy defaultpol ldap/${TEST_HOST_FQDN}" > /dev/null || die 339 | kadmin.local -q "ktadd -k /etc/krb5.ldap.keytab ldap/${TEST_HOST_FQDN}" > /dev/null || die 340 | chown root:openldap /etc/krb5.ldap.keytab || die 341 | chmod 0640 /etc/krb5.ldap.keytab || die 342 | sed -i '/KRB5_KTNAME=/d' /etc/default/slapd || die 343 | # sysv init 344 | echo "export KRB5_KTNAME=/etc/krb5.ldap.keytab" >> /etc/default/slapd 345 | # systemd 346 | echo "KRB5_KTNAME=/etc/krb5.ldap.keytab" >> /etc/default/slapd 347 | echo "OK" 348 | 349 | 350 | printf "Updating apparmor profile for slapd ... " 351 | if [ -e /etc/apparmor.d/usr.sbin.slapd ]; then 352 | mkdir -p /etc/apparmor.d/local 353 | echo "/etc/krb5.ldap.keytab kr," >> /etc/apparmor.d/local/usr.sbin.slapd 354 | apparmor_parser -r /etc/apparmor.d/usr.sbin.slapd 355 | fi 356 | echo "OK" 357 | 358 | 359 | printf "Restarting slapd ... " 360 | invoke-rc.d slapd restart || die 361 | echo "OK" 362 | 363 | 364 | printf "Creating HTTP server principal ... " 365 | kadmin.local -q "addprinc -randkey -policy defaultpol HTTP/${TEST_HOST_FQDN}" > /dev/null || die 366 | kadmin.local -q "modprinc +ok_as_delegate HTTP/${TEST_HOST_FQDN}" > /dev/null || die 367 | kadmin.local -q "modprinc +ok_to_auth_as_delegate HTTP/${TEST_HOST_FQDN}" > /dev/null || die 368 | kadmin.local -q "ktadd -k /etc/krb5.http.keytab HTTP/${TEST_HOST_FQDN}" > /dev/null || die 369 | chown root:www-data /etc/krb5.http.keytab || die 370 | chmod 0640 /etc/krb5.http.keytab || die 371 | echo "OK" 372 | 373 | 374 | printf "Setting delegation permissions via LDAP ... " 375 | if ! ldapmodify -x -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" > /dev/null < /etc/nginx/sites-available/kerberos 389 | # SPNEGO/Kerberos server test configuration 390 | # 391 | server { 392 | listen 8080; 393 | listen [::]:8080; 394 | 395 | root /var/www/kerberos; 396 | 397 | index index.php; 398 | 399 | server_name ${TEST_HOST_FQDN}; 400 | 401 | location /noauth.php { 402 | include snippets/fastcgi-php.conf; 403 | fastcgi_pass unix:/run/php/php-fpm.sock; 404 | auth_gss off; 405 | } 406 | 407 | location /auth.php { 408 | include snippets/fastcgi-php.conf; 409 | fastcgi_pass unix:/run/php/php-fpm.sock; 410 | auth_gss on; 411 | auth_gss_realm ${KERBEROS_REALM}; 412 | auth_gss_keytab /etc/krb5.http.keytab; 413 | auth_gss_service_name HTTP/${TEST_HOST_FQDN}; 414 | auth_gss_allow_basic_fallback off; 415 | auth_gss_authorized_principal alice@${KERBEROS_REALM}; 416 | auth_gss_format_full on; 417 | fastcgi_param HTTP_AUTHORIZATION ""; 418 | fastcgi_param KRB5CCNAME \$krb5_cc_name; 419 | auth_gss_service_ccache /tmp/krb5cc_nginx; 420 | } 421 | 422 | location /fallback.php { 423 | include snippets/fastcgi-php.conf; 424 | fastcgi_pass unix:/run/php/php-fpm.sock; 425 | auth_gss on; 426 | auth_gss_realm ${KERBEROS_REALM}; 427 | auth_gss_keytab /etc/krb5.http.keytab; 428 | auth_gss_service_name HTTP/${TEST_HOST_FQDN}; 429 | auth_gss_allow_basic_fallback on; 430 | auth_gss_authorized_principal bob@${KERBEROS_REALM}; 431 | auth_gss_format_full on; 432 | fastcgi_param HTTP_AUTHORIZATION ""; 433 | fastcgi_param KRB5CCNAME \$krb5_cc_name; 434 | auth_gss_service_ccache /tmp/krb5cc_nginx; 435 | } 436 | 437 | location /delegate.php { 438 | include snippets/fastcgi-php.conf; 439 | fastcgi_pass unix:/run/php/php-fpm.sock; 440 | auth_gss on; 441 | auth_gss_realm ${KERBEROS_REALM}; 442 | auth_gss_keytab /etc/krb5.http.keytab; 443 | auth_gss_service_name HTTP/${TEST_HOST_FQDN}; 444 | auth_gss_allow_basic_fallback off; 445 | auth_gss_authorized_principal alice@${KERBEROS_REALM}; 446 | auth_gss_format_full on; 447 | fastcgi_param HTTP_AUTHORIZATION ""; 448 | fastcgi_param KRB5CCNAME \$krb5_cc_name; 449 | auth_gss_service_ccache /tmp/krb5cc_nginx; 450 | auth_gss_delegate_credentials on; 451 | auth_gss_constrained_delegation on; 452 | } 453 | } 454 | EOF 455 | then 456 | die 457 | fi 458 | ln -s /etc/nginx/sites-available/kerberos /etc/nginx/sites-enabled/ || die 459 | mkdir -p /var/www/kerberos || die 460 | echo "OK" 461 | 462 | 463 | printf "Writing noauth.php ... " 464 | if ! cat <<'EOF' > /var/www/kerberos/noauth.php 465 | 468 | EOF 469 | then 470 | die 471 | fi 472 | echo "OK" 473 | 474 | 475 | printf "Writing auth.php ... " 476 | if ! cat <<'EOF' > /var/www/kerberos/auth.php 477 | 485 | EOF 486 | then 487 | die 488 | fi 489 | echo "OK" 490 | 491 | 492 | printf "Writing fallback.php ... " 493 | if ! cat <<'EOF' > /var/www/kerberos/fallback.php 494 | 502 | EOF 503 | then 504 | die 505 | fi 506 | echo "OK" 507 | 508 | 509 | printf "Writing delegate.php ... " 510 | if ! cat < /var/www/kerberos/delegate.php 511 | 558 | EOF 559 | then 560 | die 561 | fi 562 | echo "OK" 563 | 564 | 565 | # For example, if php-fpm was already running when libsasl2-modules-gssapi-mit 566 | # was installed, it won't pick up the new GSSAPI capabilities until it has been 567 | # restarted...so let's restart all services that might use SASL/GSSAPI in our 568 | # tests. 569 | printf "Restarting nginx and PHP-FPM ... " 570 | systemctl restart nginx 571 | systemctl restart "php*-fpm.service" 572 | sleep 5 573 | echo "OK" 574 | 575 | 576 | echo "" 577 | echo "=== Setup complete, start tests ===" 578 | echo "" 579 | 580 | test_path() 581 | { 582 | SUBURL="$1" 583 | EXPECT1="$2" 584 | EXPECT2="$3" 585 | 586 | printf "curl %s, no negotiate: http status (expect %s)=" "${SUBURL}" "${EXPECT1}" 587 | rm -f "${CURL_OUTPUT}" 588 | CODE="$($CURL_NONEGOTIATE -w "%{http_code}" "http://${TEST_HOST_FQDN}:8080/${SUBURL}")" || true 589 | printf "%s ... " "${CODE}" 590 | if [ "$CODE" = "${EXPECT1}" ]; then 591 | echo "OK" 592 | else 593 | EX=1 594 | echo "FAILED" 595 | if [ -e "${CURL_OUTPUT}" ]; then 596 | echo "HTTP body:" 597 | cat "${CURL_OUTPUT}" 598 | echo "" 599 | fi 600 | fi 601 | 602 | printf "curl %s, negotiate: http status (expect %s)=" "${SUBURL}" "${EXPECT2}" 603 | rm -f "${CURL_OUTPUT}" 604 | CODE="$($CURL_NEGOTIATE -w "%{http_code}" "http://${TEST_HOST_FQDN}:8080/${SUBURL}")" || true 605 | printf "%s ... " "${CODE}" 606 | if [ "$CODE" = "${EXPECT2}" ]; then 607 | echo "OK" 608 | else 609 | EX=1 610 | echo "FAILED" 611 | if [ -e "${CURL_OUTPUT}" ]; then 612 | echo "HTTP body:" 613 | cat "${CURL_OUTPUT}" 614 | echo "" 615 | fi 616 | fi 617 | } 618 | 619 | test_basic() 620 | { 621 | SUBURL="$1" 622 | EXPECT1="$2" 623 | EXPECT2="$3" 624 | 625 | printf "curl %s, incorrect basic auth: http status (expect %s)=" "${SUBURL}" "${EXPECT1}" 626 | rm -f "${CURL_OUTPUT}" 627 | CODE="$($CURL_NONEGOTIATE -u "bob:InVaLiD" -w "%{http_code}" "http://${TEST_HOST_FQDN}:8080/${SUBURL}")" || true 628 | printf "%s ... " "${CODE}" 629 | if [ "$CODE" = "${EXPECT1}" ]; then 630 | echo "OK" 631 | else 632 | EX=1 633 | echo "FAILED" 634 | if [ -e "${CURL_OUTPUT}" ]; then 635 | echo "HTTP body:" 636 | cat "${CURL_OUTPUT}" 637 | echo "" 638 | fi 639 | fi 640 | 641 | printf "curl %s, basic auth: http status (expect %s)=" "${SUBURL}" "${EXPECT2}" 642 | rm -f "${CURL_OUTPUT}" 643 | CODE="$($CURL_NONEGOTIATE -u "bob:${KRB_BOB_PW}" -w "%{http_code}" "http://${TEST_HOST_FQDN}:8080/${SUBURL}")" || true 644 | printf "%s ... " "${CODE}" 645 | if [ "$CODE" = "${EXPECT2}" ]; then 646 | echo "OK" 647 | else 648 | EX=1 649 | echo "FAILED" 650 | if [ -e "${CURL_OUTPUT}" ]; then 651 | echo "HTTP body:" 652 | cat "${CURL_OUTPUT}" 653 | echo "" 654 | fi 655 | fi 656 | 657 | } 658 | 659 | test_ldapwhoami() 660 | { 661 | LDAP_EXPECTED="dn:uid=${1},cn=gss-spnego,cn=auth" 662 | 663 | printf "Result of ldapwhoami via delegation ... " 664 | if [ -e "${CURL_OUTPUT}" ]; then 665 | LDAP_WHOAMI="$(cat "${CURL_OUTPUT}")" 666 | if [ "${LDAP_WHOAMI}" != "${LDAP_EXPECTED}" ]; then 667 | printf "%s != %s ... " "${LDAP_WHOAMI}" "${LDAP_EXPECTED}" 668 | EX=1 669 | echo "FAILED" 670 | else 671 | printf "%s ... " "${LDAP_WHOAMI}" 672 | echo "OK" 673 | fi 674 | else 675 | EX=1 676 | echo "FAILED" 677 | fi 678 | } 679 | 680 | 681 | printf "Destroying Kerberos tickets ... " 682 | kdestroy -q > /dev/null 2>&1 || true 683 | echo "OK" 684 | test_basic "fallback.php" 401 200 685 | test_path "fallback.php" 401 401 686 | test_path "noauth.php" 200 200 687 | test_path "auth.php" 401 401 688 | test_path "delegate.php" 401 401 689 | 690 | 691 | echo "" 692 | printf "Obtaining Kerberos ticket for alice ... " 693 | if kinit -kt krb5.alice.keytab alice; then 694 | echo "OK" 695 | else 696 | EX=1 697 | echo "FAILED" 698 | fi 699 | test_basic "fallback.php" 401 200 700 | test_path "fallback.php" 401 403 701 | test_path "noauth.php" 200 200 702 | test_path "auth.php" 401 200 703 | test_path "delegate.php" 401 200 704 | test_ldapwhoami "alice" 705 | 706 | 707 | echo "" 708 | printf "Obtaining Kerberos ticket for mallory ... " 709 | kdestroy -q > /dev/null 2>&1 || true 710 | if kinit -kt krb5.mallory.keytab mallory; then 711 | echo "OK" 712 | else 713 | EX=1 714 | echo "FAILED" 715 | fi 716 | test_basic "fallback.php" 401 200 717 | test_path "fallback.php" 401 403 718 | test_path "noauth.php" 200 200 719 | test_path "auth.php" 401 403 720 | test_path "delegate.php" 401 403 721 | 722 | 723 | echo "" 724 | printf "Obtaining Kerberos ticket for bob ... " 725 | kdestroy -q > /dev/null 2>&1 || true 726 | if echo "${KRB_BOB_PW}" | kinit bob > /dev/null 2>&1; then 727 | echo "OK" 728 | else 729 | EX=1 730 | echo "FAILED" 731 | fi 732 | test_basic "fallback.php" 401 200 733 | test_path "fallback.php" 401 200 734 | test_path "noauth.php" 200 200 735 | test_path "auth.php" 401 403 736 | test_path "delegate.php" 401 403 737 | 738 | 739 | echo "" 740 | printf "Removing delegation permissions via LDAP ... " 741 | if ! ldapmodify -x -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" > /dev/null < /dev/null 2>&1 || true 755 | echo "OK" 756 | test_path "delegate.php" 401 401 757 | 758 | 759 | echo "" 760 | printf "Obtaining Kerberos ticket for alice ... " 761 | if kinit -kt krb5.alice.keytab alice; then 762 | echo "OK" 763 | else 764 | EX=1 765 | echo "FAILED" 766 | fi 767 | test_path "delegate.php" 401 500 768 | 769 | 770 | echo "" 771 | printf "Obtaining Kerberos ticket for mallory ... " 772 | kdestroy -q > /dev/null 2>&1 || true 773 | if kinit -kt krb5.mallory.keytab mallory; then 774 | echo "OK" 775 | else 776 | EX=1 777 | echo "FAILED" 778 | fi 779 | test_path "delegate.php" 401 403 780 | 781 | echo "" 782 | printf "Re-adding delegation permissions via LDAP ... " 783 | if ! ldapmodify -x -D "${LDAP_ADMIN_DN}" -w "${LDAP_ADMIN_PW}" > /dev/null < /dev/null 2>&1 || true 798 | if kinit -kt krb5.alice.keytab alice; then 799 | echo "OK" 800 | else 801 | EX=1 802 | echo "FAILED" 803 | fi 804 | test_path "delegate.php" 401 200 805 | test_ldapwhoami "alice" 806 | 807 | 808 | echo "" 809 | if [ "${EX}" -ne 0 ]; then 810 | echo "=== journalctl nginx ===" 811 | journalctl -n all -xu nginx.service || true 812 | 813 | echo "=== /etc/nginx/sites-available/kerberos ===" 814 | cat /etc/nginx/sites-available/kerberos 815 | 816 | echo "=== error.log ===" 817 | cat /var/log/nginx/error.log 818 | 819 | echo "=== access.log ===" 820 | cat /var/log/nginx/access.log 821 | 822 | echo "=== journalctl slapd ===" 823 | journalctl -n all -xu slapd.service || true 824 | 825 | echo "=== slapcat ===" 826 | slapcat 827 | 828 | echo "=== ldapwhoami ===" 829 | ldapwhoami -Y GSSAPI -v -H "ldap://${TEST_HOST_FQDN}/" 830 | 831 | echo "=== klist ===" 832 | klist 833 | 834 | echo "=== /etc/krb* ===" 835 | ls -al /etc/krb* 836 | fi 837 | 838 | exit ${EX} 839 | --------------------------------------------------------------------------------