Project

General

Profile

Mod auth » History » Revision 76

Revision 75 (gstrauss, 2021-05-13 17:41) → Revision 76/91 (gstrauss, 2021-08-25 17:29)

h1. Module mod_auth - Using Authentication 

 {{>toc}} 

 h2. Description 

 "Authentication":https://en.wikipedia.org/wiki/Authentication and "Authorization":https://en.wikipedia.org/wiki/Authorization are very important concepts.    lighttpd provides multiple methods and backends for authentication and authorization.    You need not understand them all.    Instead, choose those that meet your requirements. 

 h2. Quick Start 

 Here is a simplistic configuration which is +not very secure+, but is a starting point to get something working and then improve it.    Create the auth file and start lighttpd with the following configuration.    The entire site will require authentication.    Open your browser to the site and log in with username "agent007" and password "secret". 
 @echo "agent007:secret" > /tmp/lighttpd-plain.user@    # insecure location; temporary; insecure password; do not use outside test environment 
 <pre> 
 server.modules += ("mod_auth", "mod_authn_file") 
 auth.backend = "plain" 
 auth.backend.plain.userfile = "/tmp/lighttpd-plain.user"    # insecure location; temporary; FIX to something better 
 auth.require = ( "" => ("method" => "basic", "realm" => "example", "require" => "valid-user") ) 
 </pre> 

 h2. HTTP Auth methods 

 lighttpd supports authentication methods described by RFC 7616 and RFC 7617: 

 h3. HTTP Basic auth 

 The HTTP Basic auth method transfers the username and the password in cleartext over the network (base64 encoded).    It is strongly recommended that HTTP Basic auth be used over an encrypted channel (HTTPS) between client and server. 

 HTTP Digest Auth should generally be preferred over HTTP Basic Auth. 

 h3. HTTP Digest auth 

 The HTTP Digest auth method only transfers a hashed value over the network which is much safer than HTTP Basic auth, but still cryptographically weak.    It is strongly recommended that HTTP Digest auth be used over an encrypted channel (HTTPS) between client and server. 

 HTTP Digest Auth should generally be preferred over HTTP Basic Auth. 


 h2. HTTP Auth backends 

 Depending on the method lighttpd provides various way to store the credentials used for authentication.    Some types of storage, such as storing SHA-256 digests, are safer than storing plain-text, but require a bit more effort to set up and to maintain.    Some backends require more effort for initial setup, but then are easier to maintain for larger numbers of users.    lighttpd provides many options because there is no one-size-fits-all, and there are too many choices for this document to recommend one specific backend over another.    That said, if you are comfortable with using a database and have multiple users, consider using the DBI backend (mod_authn_dbi) 

 For HTTP Basic auth: 
 * plain 
 * htpasswd  
 * htdigest 
 * dbi 
 * ldap 
 * gssapi 
 * mysql (deprecated; replaced by dbi) 
 * pam 
 * sasl 

 For HTTP Digest auth: 
 * plain 
 * htdigest 
 * dbi 

 h3. plain (mod_authn_file) 

 A file which contains username and the cleartext password separated by a colon. Each entry is terminated by a single newline.  
 e.g.: @agent007:secret@ 

 h3. htpasswd (mod_authn_file) 

 A file which contains username and the crypt()'ed password separated by a colon. Each entry is terminated by a single newline.  
 e.g.: @agent007:XWY5JwrAVBXsQ@ 

 You can use htpasswd from the apache distribution to manage those files.    @htpasswd -h@ has flags to select different algorithms (such as MD5, SHA1, SHA-256, SHA-512, ...) 
 @$ htpasswd lighttpd.user.htpasswd agent007@ 

 h3. htdigest (mod_authn_file) 

 A file which contains a line per user identification. Each line contains the username, realm, and (MD5 or SHA-256) hash value separated by colons.    The hash value is the checksum of a string concatenating the username, realm, and password, separated by colons, e.g.: with @user=agent007@ @realm='download area'@ @pass='secret'@, the hash is of the string @agent007:download area:secret@ (Note: prefer @printf@ over @echo@ to avoid accidentally hashing a newline along with the string.)  

 You can use htdigest from the apache distribution to manage an htdigest file. 
 @$ htdigest lighttpd.user.htdigest 'download area' agent007@ 

 Option for MD5: if you provide @$user@, @$realm@, and @$pass@: 
 @$ printf "%s:%s:%s\n" "$user" "$realm" "$(printf "%s" "$user:$realm:$pass" | md5sum | awk '{print $1}')"@ 
 @agent007:download area:8364d0044ef57b3defcfa141e8f77b65@ 

 Option for SHA-256: if you provide @$user@, @$realm@, and @$pass@: 
 @$ printf "%s:%s:%s\n" "$user" "$realm" "$(printf "%s" "$user:$realm:$pass" | sha256sum | awk '{print $1}')"@ 
 @agent007:download area:a8cad3bc8ff829e27b76aeffc1d722d45c4bcb43876515d56688f5bdd92a829e@ 

 For another option, see user-contributed script [[Docs_ModAuth#lightdigest|lightdigest]]. 

 h3. dbi (mod_authn_dbi) (since lighttpd 1.4.56) 

 The DBI backend is recommended for using a database to store username, realm, and encrypted (crypt()) or hashed (message digests) passwords.    Optionally, groups may also be stored.    This module supports MySQL/MariaDB, Postgres, SQLite3, and possible other databases with "libdbi-drivers":http://libdbi-drivers.sourceforge.net/    This employs more secure storage parameters than does the now-deprecated mod_authn_mysql. 

 As a best practice, password storage is required to be encrypted or hashed with a salt (or equivalent).    If compiled to use libcrypt, the "Modular Crypt Format (MCF)":https://passlib.readthedocs.io/en/stable/modular_crypt_format.html is passively supported.    If storing hashes (message digests), the hash is the message digest of the combination of @$user:$realm:$pass@, and the unique @$user:$realm:@ combination fills in as the salt.    (e.g. refer to the htdigest format above, with user, realm, and hash in separate database columns).    Using this combination, the same database table can be used for both HTTP Basic auth and HTTP Digest auth, and the database can avoid the presence of unencrypted (plain-text) password in persistent storage.    SHA-256 is the recommended message digest algorithm.    While MD5 is +not secure+ and is +not recommended+, MD5 is supported for compatibility.    Many clients do not yet support SHA-256 with HTTP Digest auth and although lighttpd supports @"algorithm" => "MD5|SHA-256"@ to send multiple alternate digest challenges, some clients do not support multiple challenges, or blindly take the first challenge. 

 To secure the hashed data in the database, it is recommended to use SHA-256 or crypt() with an appropriately strong algorithm (*not MD5*).    SHA-256 or crypt() (or MD5, if you must) is used to store data in the database even when using HTTP Basic auth.    +Even better is to use HTTP Digest auth with SHA-256 as long as it is used with known clients which support SHA-256.+    If clients are unknown and must be assumed to support only MD5, then you must make a choice and trade-off.    If the authentication is over TLS (https://...), then HTTP Basic auth with the database storing SHA-256 hashes (or crypt()ed passwords) might be a better choice than HTTP Digest auth with MD5 hashes (which are weak and insecure), unless your clients are traversing a man-in-the-middle (MITM) TLS intercepting proxy which will expose the plain-text password to the proxy in the decrypted TLS.    If using http://... (*not* using TLS), then HTTP Digest auth is slightly safer to obscure plaintext passwords that would otherwise be sent in the clear by clients.    If the digest algorithm is MD5, please keep in mind that the safety factor is marginal since MD5 is still insecure for someone determined to sniff the password and then to (easily) crack the password from the weak MD5 hash. 

 Sample SQL snippet for generating SHA-256 digest when inserting passwords into databases or generating views: 
 MySQL/MariaDB: @SHA2(CONCAT(username,':',realm,':',plaintext_password), 256)@ 
 PostgreSQL 11: @sha256(CONCAT(username,':',realm,':',plaintext_password))@ 
 Ideally, plaintext passwords should not be present on the web server, but if they are, and are present in the database, then it is a more secure practice for the lighttpd database user to be GRANT'ed privileges to read a restricted VIEW containing user, realm, and hashes; the lighttpd database user should not be granted access to tables containing the plaintext passwords if doing so can be avoided. 


 h3. ldap (mod_authn_ldap) 

 The ldap backend performs the following steps to authenticate a user 

 # Init the LDAP connection 
 # Set Protocol version to LDAPv3 
 # If StartTLS if configured -> Configure CA certificate if supplied 
 # If StartTLS if configured -> Activate TLS using StartTLS 
 # If Bind DN is included -> Simple bind with Bind-DN and Bind-Password 
 # If there is no Bind-DN -> Simple bind anonymously 
 # Try up to two times a SUBTREE search of the base-DN with the filter applied. 
 # Retrieve the DN of the user matching the filter. 
 # Finally, re-init the connection (following the steps above), this time using the DN found using the filter and the password supplied by the user. 

 If all 9 steps are performed without any error the user is authenticated. 

 h3. gssapi (mod_authn_gssapi) (kerberos5) (since lighttpd 1.4.42) 

 The gssapi backend authenticates the user against Kerberos5 infrastructure 

 h3. mysql (mod_authn_mysql) (since lighttpd 1.4.42) 

 The mysql backend authenticates the user against MySQL/MariaDB infrastructure 

 * *Deprecated*    Please migrate to use mod_authn_dbi. 

 * *Note* mod_authn_dbi requires that database store the hash of "username:realm:password" so that the value can be used for both Basic Auth and for Digest Auth. 
 This contrasts to mod_authn_mysql for which only one of Basic Auth or Digest Auth may be used at a time.    This is because mod_authn_mysql expects the database to store the _saltless_ (less secure) hash of only the password for Basic Auth, and expects the database to store the hash of "username:realm:password" if using Digest Auth. 

 h3. pam (mod_authn_pam) (since lighttpd 1.4.51) 

 The pam backend authenticates the user against PAM infrastructure, and requires that lighttpd be run as root.    Using mod_authn_pam is not recommended except for special-purpose systems where using PAM is required for integration with the existing primary authentication mechanism for the system, e.g. using PAM which is configured with a RADIUS backend to PAM.    In general, the system password database should not be directly used by web services if the user accounts have other privileges or access beyond the intended limited access required by the web service. 
 @auth.backend.pam.opts = ("service" => "http")@    # (default) 

 @/etc/pam.d/http@ (example) 
 <pre> 
   auth        sufficient     pam_unix.so nodelay try_first_pass 
   auth        requisite      pam_succeed_if.so uid >= 1000 quiet_success 
   auth        required       pam_deny.so 

   account     required       pam_unix.so 
   account     required       pam_access.so accessfile=/etc/security/http.access.conf 
 </pre> 
 To create @/etc/security/http.access.conf@ to define user/group access, see "pam_access":http://linux-pam.org/Linux-PAM-html/sag-pam_access.html 

 h3. sasl (mod_authn_sasl) (since lighttpd 1.4.48) 

 The sasl backend authenticates the user against SASL infrastructure 


 h2. Caching 

 @auth.cache = ("max-age" => "600")@    (default: inactive; no caching) (since lighttpd 1.4.56) 
 cache passwords/digests in memory to reduce load on the backend.    max-age is in seconds.    cache is checked for expiration every 8 seconds, so actual caching may be up to 8 seconds longer than configured max-age. 

 h2. Configuration template 

 After setting up the backend, edit the authentication configuration file to reflect your backend selected.    The following is a configuration template. 

 <pre> 
 ################ 

 ## type of backend  
 # plain, htpasswd, htdigest (mod_authn_file) 
 # dbi (mod_authn_dbi) 
 # ldap (mod_authn_ldap) 
 # gssapi (mod_authn_gssapi) 
 # mysql (mod_authn_mysql) 
 # pam (mod_authn_pam) 
 # sasl (mod_authn_sasl) 

 ################ 

 ## for plain, htpasswd, htdigest (mod_authn_file) 
 server.modules += ( "mod_authn_file" ) 

 ## for plain filename of the password storage for plain 
 auth.backend = "plain" 
 auth.backend.plain.userfile = "/path/to/lighttpd-plain.user" 

 ## for htpasswd 
 auth.backend = "htpasswd" 
 auth.backend.htpasswd.userfile = "/path/to/lighttpd-htpasswd.user" 

 ## for htdigest 
 auth.backend = "htdigest" 
 auth.backend.htdigest.userfile = "/path/to/lighttpd-htdigest.user" 

 ################ 

 ## for ldap 
 # the $ in auth.backend.ldap.filter is replaced by the  
 # 'username' from the login dialog 
 # since lighttpd 1.4.46, '?' can be used as placeholder 
 # instead of '$', e.g. "(uid=?)" 
 server.modules += ( "mod_authn_ldap" ) 
 auth.backend = "ldap" 
 auth.backend.ldap.hostname = "localhost" 
 auth.backend.ldap.base-dn    = "dc=my-domain,dc=com" 
 auth.backend.ldap.filter     = "(uid=$)" 
 # if enabled, startTLS needs a valid (base64-encoded) CA  
 # certificate unless the certificate has been stored 
 # in a c_hashed directory and referenced in ldap.conf 
 auth.backend.ldap.starttls     = "enable" 
 auth.backend.ldap.ca-file     = "/etc/CAcertificate.pem" 
 # If you need to use a custom bind to access the server 
 auth.backend.ldap.bind-dn    = "uid=admin,dc=my-domain,dc=com" 
 auth.backend.ldap.bind-pw    = "mysecret" 
 # If you want to allow empty passwords 
 # "disable" for requiring passwords, "enable" for allowing empty passwords 
 auth.backend.ldap.allow-empty-pw = "disable" 
 # LDAP group (https://redmine.lighttpd.net/issues/1817) 
 #auth.backend.ldap.groupmember = "mygroup" 
 # LDAP network timeout in microseconds (2000000 us = 2 sec) (OpenLDAP-specific) (since 1.4.56) 
 #auth.backend.ldap.timeout = "2000000" 

 ################ 

 ## for gssapi 
 server.modules += ( "mod_authn_gssapi" ) 
 auth.backend = "gssapi" 
 auth.backend.gssapi.keytab = "/path/to/lighttpd.keytab" 
 auth.backend.gssapi.principal = "myhost" 

 ################ 

 # DBI 
 server.modules += ( "mod_authn_dbi" ) 
 auth.backend = "dbi"  

 # (with PostgreSQL) 
 #auth.backend.dbi += ( 
 #        # required 
 #        "sql"             => "SELECT passwd FROM users WHERE user='?' AND realm='?'", 
 #        "dbtype"          => "pgsql",         # DBI database driver string name 
 #        "dbname"          => "lighttpd", 
 #        # optional 
 #        "username"        => "lighttpd",      # (some dbtype do not require username, e.g. sqlite) 
 #        "password"        => "",              # (default: empty) 
 #        "socket"          => "/path/sock",    # (default: dbtype default) 
 #        "host"            => "localhost",     # (if set, overrides "socket") 
 #        "port"            => 5432,            # (default: dbtype default) 
 #        "encoding"        => "UTF-8",         # (default: dbtype default) 
 #) 

 # (with SQLite3) 
 # auth.backend.dbi += ( 
 #     "sql" => "SELECT passwd FROM users WHERE user='?' AND realm='?'" 
 #     "dbtype" => "sqlite3", 
 #     "dbname" => "auth.db", 
 #     "sqlite3_dbdir" => "/path/to/sqlite/dbs/" 
 # ) 

 # feature: mod_authn_dbi will substitute username, then realm, then algorithm ("SHA-256" or "MD5") 
 #     for the first, second (optional), and third (optional) '?' in the SQL statement. 
 #     "SELECT passwd FROM users WHERE user='?' AND realm='?' and algorithm='?'" 
 # feature: since mod_authn_dbi permits you to specify the SQL statement, you may also 
 #     use the SQL query to combine authentication and authorization using groups in the 
 #     database, if you so choose.  
 #     "SELECT passwd FROM users WHERE user='?' AND realm='?' and group='administrators_group'" 

 ################ 

 ## for mysql (Deprecated: please migrate to use mod_authn_dbi) 
 server.modules += ( "mod_authn_mysql" ) 
 auth.backend = "mysql" 
 #auth.backend.mysql.host = "" 
 #auth.backend.mysql.user = "" 
 #auth.backend.mysql.pass = "" 
 #auth.backend.mysql.db = "" 
 #auth.backend.mysql.port = "" 
 #auth.backend.mysql.socket = "" 
 auth.backend.mysql.users_table = "mysql_users_table" 
 #auth.backend.mysql.col_user = "user" 
 #auth.backend.mysql.col_pass = "password" 
 #auth.backend.mysql.col_realm = "realm" 
 # defaults above result in query: SELECT password FROM mysql_users_table WHERE user='%s' AND realm='%s' 

 ################ 

 ## for pam 
 server.modules += ( "mod_authn_pam" ) 
 auth.backend = "pam" 
 #auth.backend.pam.opts = ( "service" => "http" )    # default "http" 

 ################ 

 ## for sasl 
 server.modules += ( "mod_authn_sasl" ) 
 auth.backend = "sasl" 
 #auth.backend.sasl.opts = ( "service"          => "http",        # default "http" 
 #                             "fqdn"             => "hostname",    # default current host 
 #                             "pwcheck_method" => "saslauthd", # default "saslauthd", else one of "saslauthd","auxprop","sasldb"  
 #                             "sasldb_path"      => "path-to-db" # if needed 
 #                           ) 

 ################ 

 # check REMOTE_USER (if set) against require rules prior to applying auth.backend 
 # REMOTE_USER might be set by another module, e.g. mod_openssl client cert verification 
 # and REMOTE_USER configured with ssl.verifyclient.username) 
 # (since lighttpd 1.4.46) 
 #auth.extern-authn = "enable" 

 ################ 
 ################ 

 ## restrictions 
 # set restrictions: 
 # 
 # auth.require = 
 # ( <url-path-prefix> => 
 #     ( "method" => "digest"/"basic", 
 #       "realm" => <realm>, 
 #       "require" => "user=<username>" ) 
 # ) 
 # 
 # <url-path-prefix> is url-path prefix to match, e.g. "/" 
 # 
 # <algorithm> is the digest algorithm to use with "method" => "digest" 
 #     The default digest algorithm MD5 is no longer considered secure. 
 #     lighttpd can be configured to use SHA-256 (since lighttpd 1.4.54) 
 #     If not specified, MD5 is used for backwards compatibility. 
 #     The examples below use "algorithm" => "SHA-256", though some 
 #     clients do not support SHA-256 digests.    lighttpd could provide 
 #     both challenges using "SHA-256|MD5", but some clients do not support 
 #     multiple digest challenges, so the default remains 
 #     "algorithm" => "MD5" if not otherwise specified. 
 # 
 # "algorithm" => "SHA-256" # (optional) (since 1.4.54) 
 # 
 # <realm> is a string to display in the dialog  
 #           presented to the user and is also used for the  
 #           digest-algorithm and has to match the realm in the  
 #           htdigest file (if used) 
 # 
 # "require" => "valid-user" will authorize any authenticated user 
 # 
 # "nonce-secret" => "my-extra-secret" # (optional) (since lighttpd 1.4.56) 
 #           If "nonce-secret" is set, the secret is used to validate that 
 #           the server generated the nonce used in HTTP Digest Auth 
 #           (i.e. to detect a client attack spoofing nonces) 
 #           Please replace "my-extra-secret" with a more complex secret. 
 # 

 server.modules += ( "mod_auth" ) 

 auth.require = ( "/download/" => 
                  ( 
                  # method must be either basic or digest 
                    "method"      => "digest", 
                    "algorithm" => "SHA-256", 
                    "realm"       => "download archiv", 
                    "require"     => "user=agent007|user=agent008" 
                  ), 
                  "/server-info" => 
                  ( 
                  # limit access to server information 
                    "method"      => "digest", 
                    "algorithm" => "SHA-256", 
                    "realm"       => "download archiv", 
                    "require"     => "valid-user" 
                  ) 
                  "/protected-folder/" => 
                  ( 
                  #  
                    "method"      => "digest", 
                    "algorithm" => "SHA-256", 
                    "realm"       => "download archiv", 
                    "require"     => "valid-user" 
                  ) 
                ) 

 # Or, using regular expressions: 
 $HTTP["url"] =~ "^/download|^/server-info" { 
   auth.require = ( "" => 
                    ( 
                      "method"      => "digest", 
                      "algorithm" => "SHA-256", 
                      "realm"       => "download archiv", 
                      "require"     => "user=agent007|user=agent008" 
                    ) 
                  ) 
 } 

 # Or, if *only* using certificate based authentication along with mod_openssl configured to require client certificates 
 #    # client side authentification        
 #    ssl.verifyclient.activate = "enable" 
 #    ssl.verifyclient.enforce = "enable" 
 #    # this line instructs client cert CN value to be extracted 
 #    # for use in require user=agent007... in auth.require 
 #    ssl.verifyclient.username = "SSL_CLIENT_S_DN_CN" 
 auth.require = ( "" => 
                  ( 
                    "method"    => "extern", 
                    "realm"     => "certificate", 
                    "require" => "user=agent007|user=agent008" 
                  ) 
                ) 
 </pre> 

 h2. Warning 

 [[Docs_ModRewrite|mod_rewrite]] rules (@url.rewrite*@, not @url.rewrite-if-not-file@) are always executed before everything else, and the request gets restarted. *So your @mod_auth@ configuration must match the rewritten urls!* 

 SHA-256 digests are supported in lighttpd when lighttpd is built with crypto libraries (TLS modules or Nettle crypto library), as those libs are from where lighttpd obtains SHA-256 digest functions. 

 SHA-256 digests are supported in lighttpd, but are not well-supported by clients.    *Opera Opera browser supports SHA-256 digests*, digests, but many other popular browsers do not. 
 https://redmine.lighttpd.net/boards/2/topics/8955 
 https://redmine.lighttpd.net/boards/2/topics/8903 

 Patches have been submitted to Mozilla to add support in Firefox: 
 https://bugzilla.mozilla.org/show_bug.cgi?id=472823 "SHA 256 Digest Authentication" 
 https://bugzilla.mozilla.org/show_bug.cgi?id=281851 "CVE-2005-2395 Wrong scheme used when server offers both Basic and Digest auth" 
 https://phabricator.services.mozilla.com/D106241 "support SHA-256 HTTP Digest auth" 

 (Update) 
 *Firefox 93.0 supports SHA-256 for RFC 7616 HTTP Digest Access Authentication* 
 (Firefox 93.0 general release is scheduled for 5 Oct 2021) 

 Open issue in Chrome: 
 https://bugs.chromium.org/p/chromium/issues/detail?id=1160478 "SHA-256 for HTTP Digest Access Authentication in accordance with rfc7616" 

 h2. Limitations 

 The implementation of digest method is compliant with RFC7616 since lighttpd 1.4.41 (#1844)) 
   Excerpt from https://www.rfc-editor.org/rfc/rfc7616.txt Section 5.13: 
     _The bottom line is that any compliant implementation will be 
     relatively weak by cryptographic standards, but any compliant 
     implementation will be far superior to Basic Authentication._ 

 Even so, there are limitations and improvements that can be made. 

 * The implementation of digest method does not prevent some types of replay attacks. (*Improved in lighttpd 1.4.56* #2976) 
 * Digest algorithm="md5-sess" is not correctly implemented in lighttpd, and probably never will be, and so its use is not recommend. (#806)    Apache mod_auth_digest also does not implement algorithm="md5-sess". 
 * LDAP authentication only allows alphanumeric uid's that do not contain punctuations. i.e.) john.doe will come up as "ldap: invalid character (a-zA-Z0-9 allowed) in username: john.doe" (This issue appears to be solved since r2526. See issue #1941) (*Fixed in lighttpd 1.4.46*) 
 * As of 1.4.19 the group field inside the require directive is not yet implemented. So auth.backend.plain.groupfile is of no use at this moment. (Note: group support for LDAP is available since lighttpd 1.4.46 (#1817)) 
 * When loaded together with @mod_fastcgi@, @mod_auth@ *must* be loaded *before* @mod_fastcgi@. Or else users will experience long delays when login in and sysadmins will probably not find out the source of the problem due to the lack of meaningful error messages. 


 h2. See Also 

 * [[HowToAuthenticationFromMultipleFiles|Authenticate from Multiple Password Files]] 


 h2. lightdigest 

 user-contributed script to manage htdigest files 
 <pre> 
 #!/bin/sh 

 export PATH="/bin:/usr/bin:/usr/sbin:$PATH" 

 # when input ctrl-c, remove lockfile and exit 
 trap '[ $lockstart -eq 1 ] && unlock $pfile && exit 0 || exit 0' INT 

 pfile="/etc/lighttpd/conf.d/lighttpd.user" 
 lockstart=0  
 remove=0 

 errmsg() { 
     echo "$1" > /dev/stderr 
 } 

 user_check() { 
     check_user=$1 
     grep "^${check_user}:" ${pfile} >& /dev/null 
     return $? 
 } 

 lock() { 
     lockfile="$1" 
     lockfile="${lockfile}.lock" 

     [ -f "${lockfile}" ] && { 
         errmsg "WARNING: lock file ${lockfile} is already exists" 
         errmsg "           Wait minites for end of previous working ..." 
     } 

     while [ -f "${lockfile}" ]; do echo >& /dev/null ; done 
     touch ${lockfile}  
     lockstart=1 
 } 

 unlock() { 
     lockfile="$1" 
     lockfile="${lockfile}.lock" 

     [ -f "${lockfile}" ] && rm -f ${lockfile} && lockstart=0 
 } 

 usage() { 
     errmsg 
     errmsg "lightdigest: lighttpd htdigest password generation program" 
     errmsg "Scripted by JoungKyun.Kim <http://oops.org>" 
     errmsg 
     errmsg "Usage: $0 -[hd] -u user -p pass -r realm [-f password_file]" 
     errmsg "Options:" 
     errmsg "      -h            print this help messages" 
     errmsg "      -u user       username" 
     errmsg "      -p pass       password" 
     errmsg "      -r realm      realm name" 
     errmsg "      -f filename password file [default: /etc/lighttpd/conf.d/lighttpd.user" 
     errmsg "      -d            remove user" 
     errmsg 

     [ $lockstart -eq 1 ] && rm -f ${pfile}.lock 

     exit 1 
 }    

 opts=$(getopt df:hp:r:u: $*) 
 [ $? != 0 ] && usage 

 set -- ${opts} 
 for i 
 do 
     case "$i" in 
         -d) remove=1; shift;; 
         -f) pfile="$2"; shift; shift;; 
         -p) pass="$2"; shift; shift;; 
         -r) realm="$2"; shift; shift;; 
         -u) user="$2"; shift; shift;; 
         --) shift; break; 
     esac 
 done 

 #echo $user 
 #echo $realm 
 #echo $pass 
 #echo $pfile 
 #echo $remove 

 [ -z "$user" ] && errmsg "ERROR: User is none!!" && usage 
 [ ${remove} -eq 0 -a -z "${realm}" ] && errmsg "ERROR: Realm is none!!" && usage 

 if [ -z "${pass}" -a ${remove} -eq 0 ]; then 
     echo -n "Input new password : " 
     read newpass 
     echo -n "Reinput password for confirm : " 
     read renewpass 

     if [ "${newpass}" != "${renewpass}" ]; then 
         errmsg "ERROR: Password is not match" 
         exit 1 
     fi 

     pass=${newpass} 
 fi 

 lock ${pfile} 

 if [ ${remove} -eq 0 ]; then 
     # User Add Mode 
     #hash=$(echo -n "${user}:${realm}:${pass}" | md5sum | cut -b -32) 
     # SHA-256 is much more secure than MD5 (which is weak and insecure) 
     hash=$(echo -n "${user}:${realm}:${pass}" | sha256sum | awk '{print $1}') 
     user_check ${user} 
     already=$? 

     [ -f "${pfile}" ] && cp -af ${pfile} ${pfile}.bak 
     if [ ${already} -eq 0 ]; then 
         # already exists 
         perl -pi -e "s/^${user}:.*$/${user}:${realm}:${hash}/g" ${pfile} 
     else 
         # add new user 
         echo "${user}:${realm}:${hash}" >> ${pfile} 
     fi 
 else 
     # User Remove Mode 
     tmp_htdigest="/tmp/lighttpd-htdiges.tmp.$$" 
     cp -af ${pfile} ${pfile}.bak 
     grep -v "^${user}:" ${pfile} > ${tmp_htdigest} 
     mv -f ${tmp_htdigest} ${pfile} 
 fi 

 unlock ${pfile} 

 exit 0 
 </pre> 

 To use it (don't use realm value! getopt of some bash version has bug.) : 

 <pre> 
   # if you add or change 
   $ lightdigest -u USERNAME -r REALM_NAME -f PASSWORD_FILE_PATH 
   # if you want to remove use   
   $ lightdigest -d -u USERNAME 
 </pre>