diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index 7cd17a5..effe82d 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -78,3 +78,4 @@ version: redis: "8.6.1" immich: "v2.6.1" actualbudget: "26.3.0" + paperless: "2.20.13" diff --git a/ansible/inventory/host_vars/app.yaml b/ansible/inventory/host_vars/app.yaml index ed29b97..2f3662e 100644 --- a/ansible/inventory/host_vars/app.yaml +++ b/ansible/inventory/host_vars/app.yaml @@ -42,3 +42,4 @@ storage: redis: immich: "6379" + paperless: "6380" diff --git a/ansible/playbooks/app/site.yaml b/ansible/playbooks/app/site.yaml index 42bd1ea..af13ab4 100644 --- a/ansible/playbooks/app/site.yaml +++ b/ansible/playbooks/app/site.yaml @@ -193,6 +193,14 @@ tags: ["site", "actual-budget"] tags: ["site", "actual-budget"] + - name: Set paperless + ansible.builtin.include_role: + name: "app" + tasks_from: "services/set_paperless" + apply: + tags: ["site", "paperless"] + tags: ["site", "paperless"] + - name: Flush handlers right now ansible.builtin.meta: "flush_handlers" diff --git a/ansible/roles/app/handlers/main.yaml b/ansible/roles/app/handlers/main.yaml index d31593a..f5490a4 100644 --- a/ansible/roles/app/handlers/main.yaml +++ b/ansible/roles/app/handlers/main.yaml @@ -53,3 +53,14 @@ changed_when: false listen: "notification_restart_actual-budget" ignore_errors: true # noqa: ignore-errors + +- name: Restart paperless + ansible.builtin.systemd: + name: "paperless.service" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + changed_when: false + listen: "notification_restart_paperless" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/app/tasks/services/set_paperless.yaml b/ansible/roles/app/tasks/services/set_paperless.yaml new file mode 100644 index 0000000..4c32bb7 --- /dev/null +++ b/ansible/roles/app/tasks/services/set_paperless.yaml @@ -0,0 +1,128 @@ +--- +- name: Set redis service name + ansible.builtin.set_fact: + redis_service: "paperless" + redis_subuid: "100998" + +- name: Create redis_paperless directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ redis_subuid }}" + group: "svadmins" + mode: "0770" + loop: + - "containers/redis" + - "containers/redis/{{ redis_service }}" + - "containers/redis/{{ redis_service }}/data" + become: true + +- name: Deploy redis config file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" + dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + +- name: Deploy redis container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + register: "is_redis_conf" + +- name: Enable (Restart) redis service + ansible.builtin.systemd: + name: "redis_{{ redis_service }}.service" + state: "restarted" + enabled: true + daemon_reload: true + scope: "user" + when: is_redis_conf.changed # noqa: no-handler + +- name: Set paperless subuid + ansible.builtin.set_fact: + paperless_subuid: "100999" + +- name: Create paperless directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ paperless_subuid }}" + group: "svadmins" + mode: "0770" + loop: + - "data/containers/paperless" + - "data/containers/paperless/data" + - "data/containers/paperless/media" + - "data/containers/paperless/consume" + - "containers/paperless" + - "containers/paperless/ssl" + become: true + + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/paperless/ssl/ilnmors_root_ca.crt" + owner: "{{ paperless_subuid }}" + group: "svadmins" + mode: "0440" + become: true + notify: "notification_restart_paperless" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "PAPERLESS_SECRET_KEY" + value: "{{ hostvars['console']['paperless']['session_secret'] }}" + - name: "PAPERLESS_DBPASS" + value: "{{ hostvars['console']['postgresql']['password']['paperless'] }}" + - name: "PAPERLESS_SOCIALACCOUNT_PROVIDERS" + value: |- + { + "openid_connect": { + "SCOPE": ["openid", "profile", "email"], + "OAUTH_PKCE_ENABLED": true, + "APPS": [ + { + "provider_id": "authelia", + "name": "Authelia", + "client_id": "paperless", + "secret": "{{ hostvars['console']['paperless']['oidc']['secret'] }}", + "settings": { + "server_url": "https://authelia.ilnmors.com/.well-known/openid-configuration", + "token_auth_method": "client_secret_post" + } + } + ] + } + } + notify: "notification_restart_paperless" + no_log: true + +- name: Deploy paperless.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/paperless/paperless.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/paperless.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + notify: "notification_restart_paperless" + +- name: Enable paperless.service + ansible.builtin.systemd: + name: "paperless.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" diff --git a/ansible/roles/infra/tasks/services/set_postgresql.yaml b/ansible/roles/infra/tasks/services/set_postgresql.yaml index d5e8fe5..3de2afd 100644 --- a/ansible/roles/infra/tasks/services/set_postgresql.yaml +++ b/ansible/roles/infra/tasks/services/set_postgresql.yaml @@ -12,6 +12,7 @@ - "vaultwarden" - "gitea" - "immich" + - "paperless" - name: Create postgresql directory ansible.builtin.file: diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index cefce35..7b893a6 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -116,6 +116,7 @@ postgresql: vaultwarden: ENC[AES256_GCM,data:BPj5eFo54DTZ82n3yTIqEbm7kb/jWT0n2kZY//oV5q48eRch3C2RBuxn/Ko=,iv:DGC4ipHMyVs25gc4sNMt8LN1RsHjiR/b303vgiFoxMY=,tag:k1eb4DoRPLKvvMstSI1faQ==,type:str] gitea: ENC[AES256_GCM,data:l+pBCzyQa3000SE9z1R4htD0V0ONsBtKy92dfgsVYsZ3XlEyVJDIBOsugwM=,iv:5t/oHW1vFAmV/s2Ze/cV9Vuqo96Qu6QvZeRbio7VX2s=,tag:4zeQaXiXIzBpy+tXsxmN7Q==,type:str] immich: ENC[AES256_GCM,data:11jvxTKA/RL0DGL6y2/X092hnDohj6yTrYGK4IVojqBd1gCOBnDvUjgmx14=,iv:oBfHxsx9nxhyKY/WOuWfybxEX2bf+lHEtsaifFRS9lg=,tag:tAfkBdgQ8ZEkLIFcDICKDw==,type:str] + paperless: ENC[AES256_GCM,data:6VBrBbjVoam7SkZCSvoBTdrfkUoDghdGTiBmFLul04X/okXOHeC5zusJffY=,iv:iZumcJ3TWwZD77FzYx8THwCqC+EbnXUBrEKuPh3zgV8=,tag:u2m8SppAdxZ/duNdpuS3oQ==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -214,6 +215,17 @@ actualbudget: #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # +#ENC[AES256_GCM,data:McPUAbIUvtC1gdPaxTgAxAMCMWcLfg==,iv:Tp6idRf7he3sYzo8LW596C905JAaoTIhIoDUzSyRT0k=,tag:4mZQ0Swu1X9uuwjsRNhr2A==,type:comment] +paperless: + session_secret: ENC[AES256_GCM,data:siwCs2noeVpg9DCEZybnmo/oz11BdrHSTnHciMOu/6g=,iv:XVjhu10TIujIdUopN9+TVVqRade9EvItDWxym6YXnZs=,tag:TxLYm+4Bo7IMaTQBtMg9pQ==,type:str] + il: + password: ENC[AES256_GCM,data:9bJHf+chTg1rppgNVafNgEuvwQ69Gx+w5d65hu68q9XeeaVb2pO9HE4BOgg=,iv:1kaXBg/iOoIZxDjEVEdaMJLDtp6zQjep3vxLmIgQN5o=,tag:+MgX8Oa3tmhjx6u9aHkDfQ==,type:str] + oidc: + secret: ENC[AES256_GCM,data:wjRDVCJsINM4z5946a6uZD+6bhN5BChLMdRzgMEJFGRGFNcXd7A1p2Iqn4I=,iv:Y4QDA09L8ULKr4hhvoiduzCD8Hifo1gAnpzjCr8e520=,tag:R0RvGxYnXo3zwykXJykRug==,type:str] + hash: ENC[AES256_GCM,data:pali6WwPNhJA+6QL4O+tKv42PnpGqmojb8JQUZLqxGizv1bJSCgdUN8upCy5Ke0DYZs5P+JY5vh23xfMZFnHduGxGwOuPX6J5lYgvJRV58LqS3/+yIBBprTJyro3MwsurTTEWesgKMr8/2H9lirhaLjWUOSPxAmQ6e4wPNpHycDVyj4=,iv:cg2trI7t1MfIcMo1/M+IY6JEl2msDoKRGgAx/Y5nyGk=,tag:gnOq7sBq9z5zrRY0yhIabg==,type:str] +#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] +# +# #ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment] switch: password: ENC[AES256_GCM,data:qu0f9L7A0eFq/UCpaRs=,iv:W8LLOp3MSfd/+EfNEZNf91K8GgI5eUfVPoWTRES2C0Y=,tag:Q5FlAOfwqwJwPvd7k6i+0g==,type:str] @@ -243,7 +255,7 @@ sops: UmliaFNxVTBqRkI1QWJpWGpTRWxETW8KEY/8AfU73UOzCGhny1cNnd5dCNv7bHXt k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-03-21T06:16:49Z" - mac: ENC[AES256_GCM,data:GxUEHQrJGtAAA/Bchv2M/Pvx923zlYB0aF4Vsj/NgRZM30WydJ2lyWW7cmpfGmx/wRxsvzNfcxgeTBhKC6gZ1iLaTRuWV3uNZF28ewLr0+pElHtBvRcMV8NwOupyph6hyU/yJreN/T6TRL5XkiHERlzc5Me5JP1o2Pr9ilyl6qc=,iv:rfMebGbEx8QfX/tMNxgxGg4eDBQmHZAk4+PMlb1RXQI=,tag:oN3waTAcjEGMcRS6wsoY9w==,type:str] + lastmodified: "2026-03-24T06:37:53Z" + mac: ENC[AES256_GCM,data:+by7KiDiod7d0KtLB8jBnuTUtISLkn7WrwW/MrOGnxxqO9JnmD36HeugM782K79Rgymu0osexyvSQ2xpwfDQL/6WjfKkqxXirpeVrHFjjMFrJ3r2Wnn9GoCRf3ObJEXJD8x59IL/fsTDfzGTLaOG71I5Zs7j+LQnrm4Uj3KD6Rg=,iv:lHcuCw7a7j7CkBT183fYMhpQhx97Mz4DYrWYZQYbFNQ=,tag:yAZXT4FrAbwgkespCPdIBA==,type:str] unencrypted_suffix: _unencrypted version: 3.12.1 diff --git a/config/services/containers/app/paperless/paperless.container.j2 b/config/services/containers/app/paperless/paperless.container.j2 new file mode 100644 index 0000000..0bb79a8 --- /dev/null +++ b/config/services/containers/app/paperless/paperless.container.j2 @@ -0,0 +1,57 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Paperless + +After=redis_paperless.service +Wants=redis_paperless.service + +[Container] +Image=ghcr.io/paperless-ngx/paperless-ngx:{{ version['containers']['paperless'] }} +ContainerName=paperless +HostName=paperless +PublishPort=8001:8000/tcp + +# Volumes +Volume=%h/data/containers/paperless/data:/usr/src/paperless/data:rw +Volume=%h/data/containers/paperless/media:/usr/src/paperless/media:rw +Volume=%h/data/containers/paperless/consume:/usr/src/paperless/consume:rw +Volume=%h/containers/paperless/ssl:/etc/ssl/paperless:ro + +# General +Environment="TZ=Asia/Seoul" +Environment="PAPERLESS_URL=https://paperless.ilnmors.com" +Environment="PAPERLESS_OCR_LANGUAGE=kor+eng" +Environment="PAPERLESS_OCR_LANGUAGES=kor" +Environment="PAPERLESS_OCR_MODE=force" +# Environment="PAPERLESS_TASK_WORKERS=1" +# Environment="PAPERLESS_THREADS_PER_WORKER=1" +Secret=PAPERLESS_SECRET_KEY,type=env + +# Redis +Environment="PAPERLESS_REDIS=redis://host.containers.internal:{{ hostvars['app']['redis']['paperless'] }}" + +# Database +Environment="PAPERLESS_DBHOST={{ infra_uri['postgresql']['domain'] }}" +Environment="PAPERLESS_DBPORT={{ infra_uri['postgresql']['ports']['tcp'] }}" +Environment="PAPERLESS_DBNAME=paperless_db" +Environment="PAPERLESS_DBUSER=paperless" +Environment="PAPERLESS_DBSSLMODE=verify-full" +Environment="PAPERLESS_DBSSLROOTCERT=/etc/ssl/paperless/ilnmors_root_ca.crt" +Secret=PAPERLESS_DBPASS,type=env + +# OIDC +Environment="PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect" +Environment="PAPERLESS_SOCIAL_AUTO_SIGNUP=true" +Environment="PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=true" +Secret=PAPERLESS_SOCIALACCOUNT_PROVIDERS,type=env + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ infra_uri['postgresql']['domain'] }} {{ infra_uri['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/auth/authelia/config/authelia.yaml.j2 b/config/services/containers/auth/authelia/config/authelia.yaml.j2 index 96fc64e..df44cb1 100644 --- a/config/services/containers/auth/authelia/config/authelia.yaml.j2 +++ b/config/services/containers/auth/authelia/config/authelia.yaml.j2 @@ -197,3 +197,25 @@ identity_providers: access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' token_endpoint_auth_method: 'client_secret_basic' + # https://www.authelia.com/integration/openid-connect/clients/paperless/ + - client_id: 'paperless' + client_name: 'Paperless' + client_secret: '{{ hostvars['console']['paperless']['oidc']['hash'] }}' + public: false + authorization_policy: 'one_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - 'https://paperless.ilnmors.com/accounts/oidc/authelia/login/callback/' + scopes: + - 'openid' + - 'profile' + - 'email' + - 'groups' + response_types: + - 'code' + grant_types: + - 'authorization_code' + access_token_signed_response_alg: 'none' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_post' diff --git a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 index d970296..83b4116 100644 --- a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 @@ -47,10 +47,15 @@ immich.app.ilnmors.internal { header_up Host {http.request.header.X-Forwarded-Host} } } - budget.app.ilnmors.internal { import private_tls reverse_proxy host.containers.internal:5006 { header_up Host {http.request.header.X-Forwarded-Host} } } +paperless.app.ilnmors.internal { + import private_tls + reverse_proxy host.containers.internal:8001 { + header_up Host {http.request.header.X-Forwarded-Host} + } +} diff --git a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 index c53bb29..8521c6a 100644 --- a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 @@ -100,6 +100,15 @@ budget.ilnmors.com { } } } +paperless.ilnmors.com { + import crowdsec_log + route { + crowdsec + reverse_proxy https://paperless.app.ilnmors.internal { + header_up Host {http.reverse_proxy.upstream.host} + } + } +} # Internal domain auth.ilnmors.internal { diff --git a/docs/services/app/paperless-ngx.md b/docs/services/app/paperless-ngx.md new file mode 100644 index 0000000..04b4b11 --- /dev/null +++ b/docs/services/app/paperless-ngx.md @@ -0,0 +1,61 @@ +# paperless + +## Prerequisite + +### Create database + +- Create the password with `openssl rand -base64 32` + - Save this value in secrets.yaml in `postgresql.password.paperless` + - Access infra server to create paperless_db with `podman exec -it postgresql psql -U postgres` + +```SQL +CREATE USER paperless WITH PASSWORD 'postgresql.password.paperless'; +CREATE DATABASE paperless_db; +ALTER DATABASE paperless_db OWNER TO paperless; +``` + +### Create oidc secret and hash + +- Create the secret with `openssl rand -base64 32` +- access to auth vm + - `podman exec -it authelia sh` + - `authelia crypto hash generate pbkdf2 --password 'paperless.oidc.secret'` +- Save this value in secrets.yaml in `paperless.oidc.secret` and `paperless.oidc.hash` +- Use `client_secret_post`, django encodes the secret value wrong frequently. + +### Create session secret value + +- Create the secret with `LC_ALL=C tr -dc 'A-Za-z0-9!#%&()*+,-./:;<=>?@[\]^_{|}~'