Postgres recursive query for SSH authorized key paths - postgresql

How do I structure a recursive query to help me find all server-to-server jumps that are accessible via SSH public key authorization?
For a given SSH pubkey fingerprint, first find the list of servers/accounts that the key is authorized for. For each server found, iterate over the private keys on that server, and find the list of servers/accounts that each of those are authorized for.
Repeat this process until there's a dead end, i.e. the authorized key path ends. Return a full authorized key path (as an array) per row.
Simplified example:
CREATE TABLE sshkeys (
server text, -- server UUID
username text, -- authorized username
privkey_owner text, -- owner of private key
fingerprint text, -- SSH key fingerprint
keytype text, -- "public" or "private"
)
Server Username Owner Fingerprint Keytype
banana root key_id_james public
banana james key_id_james public
banana root key_id_root#banana private
apple root key_id_root#banana public
apple fred fred key_id_fred private
mango fred key_id_fred public
James, using his key with ID key_id_james, has access to root#banana. From there, the root#banana private key is authorized for root#apple. On apple, root can access key_id_fred, and is thus authorized for fred#mango.
So ultimately, James has access to Fred's account on mango, via root access to banana and apple. He also has access to james#banana. The two output rows would consist of arrays of {source, keyid, target} elements, looking something like:
{{NULL, key_id_james, root#banana},
{banana, key_id_root#banana, root#apple},
{apple, key_id_fred, fred#mango}},
{{NULL, key_id_james, james#banana}}
I plan to add restrictions and heuristics that should be taken into account, including whether private keys are encrypted and whether a given user has group or chmod access to another user's private key (by joining to other tables in the database). That should be easy to tack onto a working base query, however.
As for the query, I'm unable to figure out how to do the secondary step, that is: For each server initially found to be accessible via the given key, iterate over all private keys on each server and recurse. I'm not getting further than this non-working query:
WITH RECURSIVE initial_authkeys AS (
SELECT
server, username, privkey_owner, fingerprint, keytype
FROM
sshkeys
WHERE
fingerprint = 'key_id_james' AND
keytype = 'public'
UNION ALL
SELECT
ak.server, ak.username, ak.privkey_owner, ak.fingerprint, ak.keytype
FROM
sshkeys,
initial_authkeys ak
WHERE
sshkeys.fingerprint = ak.fingerprint AND
sshkeys.keytype = 'private'
)
SELECT * FROM initial_authkeys;
Any suggestions?

What's needed to make this work is a subquery expression to find the SSH fingerprints of any available private key on the server where the given pubkey is authorized. Below is a working query that includes a rudimentary heuristic to only find private keys if the user has root access to the server. It could be expanded to verify private key file permissions, group ownership, etc.
WITH RECURSIVE authkeys(server, username, fingerprint, level, auth_path) AS (
SELECT
server, username, fingerprint, 1 AS level,
ARRAY['Authorized to '||username||
'#'||server||' using keyid='||fingerprint]
FROM
sshkeys
WHERE
fingerprint = 'key_id_james' AND keytype = 'public'
UNION
SELECT
k.server, k.username, k.fingerprint, ak.level+1 AS level,
auth_path || ARRAY['Authorized to '||k.username||
'#'||k.server||' using keyid='||k.fingerprint]
FROM
sshkeys k, authkeys ak
WHERE
k.keytype = 'public' AND k.fingerprint IN (
SELECT
fingerprint
FROM
sshkeys k2
WHERE
k2.server = ak.server AND k2.keytype = 'private' AND
ak.username = 'root'
)
) SELECT * FROM authkeys;

Related

Supabase: PostgreSQL policy using a column value gives error

I am trying to set a Row Level Security policy in PostgreSQL via Supabase.
The table has two relevant fields: access (either "P" for public or "R" for restricted) and include (an array of email ids). I wish to set a policy which allows all authenticated users to access the records which are either "P" or if "R" their email must be present.
CREATE POLICY "policy_name"
ON public.table
FOR SELECT
TO authenticated
USING (
access = "P" OR
auth.email() = ANY(include)
);
This gives an error saying Column "P" does not exist
I am new to SQL expressions and will appreciate any help. Thanks
The error was because I used double quotes!!!!
The policy that works in supabase is:
(((jwt() ->> 'email'::text) = ANY (include)) OR (access = 'P'::text) OR (uid() = user_id))
Changed the auth.email() to jwt() ->> 'email' since the former is being deprecated.
I added the last uid() = user_id (a column in the table) without which it would not permit the user to add a record

Postgres/Supabase RLS - Only Admin can Insert if the ID matches from another table

I'm trying to learn Postgres RLS.
My objective: only an admin can insert an amenity, and it must match the id of the condo the admin already belongs too.
something like:
(EXISTS ( SELECT 1
FROM condos
WHERE (auth.uid() = condos.admin_id)))
This is a policy I'm adding to public.amenitites
But I'm getting an 403 when trying to POST. I've written it wrong and I'm not sure what to change.

Can't select existing value, after row update it works | [postgres:13.5-alpine3.15]

I'm using pg_admin on to select a specific user by email address. The selection looks like this
SELECT id, public_id
FROM public.users
where email = 'user1#test-inger.com';
I know that the user exists with ID 102 with the exact string for the email. But the select returns nothing. Our investigation showns that all cases for the problem contain a '-' in the email. Maybe this input is relevant.
Now if I'm using pg_admin to update the email address from 'user1#testinger.com' to 'anything#toaster.com', commit the change and revert to the initial value 'user1#testinger.com' the select find the result. It almost appears to me that the table row is corrupt.
The project runs with docker and I'm using postgres:13.5-alpine3.15.
I have now tried to bring the backup to my development env and the select works. Any ideas how this can be fixed would be apprechiated.

PostgreSQL 9.5 - Row level security / ROLE best practices

I'm tying to grasp the best way to use the new row level security feature in a multi-tenant database that supports a web application.
Currently, the application has a few different ROLEs available, depending on the action it is attempting to take.
Once the application makes a connection using its own ROLE, the application passes authentication parameters (provided by the user) into different functions that filter out rows based on the user supplied authentication parameters. The system is designed to work with thousands of users and it seems to work; however, it's defiantly clunky (and slow).
It seems that if I wanted to use the new row level security feature I would need to create a new ROLE for each real world user (not just for the web application) to access the database.
Is this correct? and if so, is it a good idea to create thousands of ROLEs in the database?
Update from a_horse_with_no_name's link in the comments (thanks, that thread is spot on):
CREATE USER application;
CREATE TABLE t1 (id int primary key, f1 text, app_user text);
INSERT INTO t1 VALUES(1,'a','bob');
INSERT INTO t1 VALUES(2,'b','alice');
ALTER TABLE t1 ENABLE ROW LEVEL SECURITY;
CREATE POLICY P ON t1 USING (app_user = current_setting('app_name.app_user'));
GRANT SELECT ON t1 TO application;
SET SESSION AUTHORIZATION application;
SET app_name.app_user = 'bob';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
1 | a | bob
(1 row)
SET app_name.app_user = 'alice';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
2 | b | alice
(1 row)
SET app_name.app_user = 'none';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
(0 rows)
Now, I'm confused by current_setting('app_name.app_user') as I was under the impression this was only for configuration parameters... where is app_name defined?
Setting security policies based on a session setting is a BAD BAD BAD idea (I hate both CAPS and bold so trust me that I mean it). Any user can SET SESSION 'app_name.app_user' = 'bob', so as soon as someone figures out that "app_name.app_user" is the door in (trust me, they will) then your whole security is out the door.
The only way that I see is to use a table accessible to your webadmin only which stores session tokens (uuid type comes to mind, cast to text for ease of use). The login() function is SECURITY DEFINER (assuming owner webadmin), setting the token as well as a session SETting, and then the table being owned by (or having appropriate privileges for) webadmin refers to that table and the session setting in its policy.
Unfortunately, you cannot use temporary (session) tables here because you cannot build policies on a temporary table so you have to use a "real" table. That is something of a performance penalty, but weigh that against the damage of a hack...
In practice:
CREATE FUNCTION login (uname text, pwd text) RETURNS boolean AS $$
DECLARE
t uuid;
BEGIN
PERFORM * FROM users WHERE user = uname AND password = pwd;
IF FOUND THEN
INSERT INTO sessions SET token = uuid_generate_v4()::text, user ....
RETURNING token INTO t;
SET SESSION "app_name.token" = t;
RETURN true;
ELSE
SET SESSION "app_name.token" = '';
RETURN false;
END IF;
END; $$ LANGUAGE plpgsql STRICT;
And now your policy would link to sessions:
CREATE POLICY p ON t1 FOR SELECT
USING (SELECT true FROM sessions WHERE token = current_setting('app_name.token'));
(Since uuids may be assumed to be unique, no need for LIMIT 1. ordering or other magic, if the uuid is in the table the policy will pass, otherwise fail.) The uuid is impossible to guess (within your lifetime anyway) and impossible to retrieve by anyone but webadmin.

pgcrypto Postgresql PGP pgp_pub_decrypt with password

I have been experiencing few problems with data encryption over pgcrypto with Postgresql 8.4.
First case : works fine :
select pgp_pub_decrypt(pgp_pub_encrypt('fsck',
dearmor(pubkey),'compress-algo=1,
cipher-algo=aes256'),dearmor(seckey)) from keytbl where keytbl.id=1
-> returns "fsck"
key 1 is pgp with no password
Second case : doesn't work
select pgp_pub_decrypt(pgp_pub_encrypt('fsck',
dearmor(pubkey),'compress-algo=1,
cipher-algo=aes256'),dearmor(seckey),'password') from keytbl where
keytbl.id=2
-> returns ERREUR: Corrupt data
When i generate keys with password pgcrypto doesn't want to decrypt the message crypted with the public key ....
Anyone got a guess ? This is driving me mad...
This appears to be a known bug in at least 8.4 and 9.0. I have avoided this in the past by avoiding using the passphrase functionality and using pgp_sym_encrypt and pgp_sym_decrypt to manage the keys' passphrases.
In general if this is giving you a problem your best option is to encrypt the keys with a passphrase separately using well-tested functions.
To give you an idea of how we do it:
create or replace function user__replace_keys
(in_public_key bytea, in_private_key bytea, in_passphrase text)
RETURNS user_key
LANGUAGE SQL AS
$$
UPDATE user_key
SET last_resort_key = pgp_pub_encrypt(
pgp_pub_decrypt(
last_resort_key,
pgp_sym_decrypt_bytea(priv_key, $3)
), $2
),
pub_key = $2,
priv_key = pgp_sym_encrypt_bytea($2, $3)
WHERE login = SESSION_USER
RETURNING *;
$$;
Note that the private key sent to the server must not be password encryted. We might actually generate it on the server or in middleware to avoid problems. This avoids bugs of the sort you are experiencing.