On the MySQL Proxy channel we get questions from time to time if the authentication can be intercepted and replaced data from a external source.
From now on, you can. For example if you want to get data from a external source (like LDAP) or want to implement roles.
Mapping Accounts to “Roles”
There isn’t much needed to implement Roles for MySQL with the help of the MySQL Proxy.
-
mysql.user
doesn’t contain users, but roles instead - the proxy maps user-accounts to role-accounts with a script like above
It works like this:
-
login to the proxy
$ mysql --user=jan --password=secret --port=4040
-
proxy looks up username password, finds a role for him
-
proxy replaces credentials ad hoc
-
mysql-server sees the role-name and role-password and lets the user in
This works nicely as long as you only have 1:n mapping between roles and users. One user has one role, but one role has many users.
All you need is a storage that has user’s password in one of three forms:
- cleartext as in the example below
- SHA1(cleartext) as you may have in LDAP
- SHA1(SHA1(cleartext)) as in
mysql.user
:SELECT CONCAT("*", SHA1(UNHEX(SHA1("pass")))), PASSWORD("pass");
How the Auth works
The MySQL Proxy has access to all protocol data and exposes it as part of different stages of the authentication to the scripting layer:
---
-- show how to use the mysql.password functions
function read_auth()
local c = proxy.connection.client
local s = proxy.connection.server
print(("for challenge %q the client sent %q"):format(
s.scramble_buffer,
c.scrambled_password
))
end
The challenge and response secures the password and is works like this:
SERVER: scramble_buffer=create_random_string()
send(scramble_buffer)
CLIENT: recv(scramble_buffer)
hash_stage1=sha1("password")
hash_stage2=sha1(hash_stage1)
scrambled_password=xor(hash_stage1, sha1(scramble_buffer, hash_stage2)
send(scrambled_password)
SERVER: recv(scrambled_password)
hash_stage1=xor(scrambled_password, sha1(scramble_buffer,hash_stage2))
candidate_hash2=sha1(hash_stage1)
check(candidate_hash2==hash_stage2)
That’s taken from sql/password.c
of the MySQL Server
Source. The hash_stage2
is the same as result of the
PASSWORD()
function without the *
and
the HEX()
and is stored in mysql.user
.
Replacing the auth response
Based on the read_auth()
from above you can also
replace the credentials on the fly:
---
-- map replace:me with root:secret
--
if c.username == "replace" and -- the username we want to map
password.check(
s.scramble_buffer,
c.scrambled_password,
password.hash(password.hash("me")) -- its valid password
) then
proxy.queries:append(1,
proto.to_response_packet({
username = "root",
response = password.scramble(s.scramble_buffer, password.hash("secret")),
charset = 8, -- default charset
database = c.default_db,
max_packet_size = 1 * 1024 * 1024
})
)
return proxy.PROXY_SEND_QUERY -- works if you use lp:mysql-proxy r694 or later
end
At
you can see the full version of it.
Let’s see if it works:
$ mysql-proxy --proxy-lua-script=tutorial-scramble.lua
$ mysql --port=4040 --user=replace --password=me
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2702
Server version: 5.1.34-log MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
root@127.0.0.1 [(none)]>
I logged in as the root
user.
Fixing the hostcheck
This can also be used to rewrite the username to something that includes his original ip. Like
...,
username = c.username .. "-" .c.src.address,
...
to make the server see something else than the proxy’s
client-address if you use the host-field in the
mysql.user
tables.